Merged in frontend-modernization (pull request #27)
Frontend modernization Approved-by: Dorian Pula <dorian.pula@amber-penguin-software.ca>
This commit is contained in:
commit
c8decceb8b
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"es2017",
|
||||||
|
"es2015",
|
||||||
|
"react",
|
||||||
|
"stage-0"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"transform-react-jsx",
|
||||||
|
{
|
||||||
|
"pragma": "h"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"module-resolver",
|
||||||
|
{
|
||||||
|
"root": [
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"alias": {
|
||||||
|
"react": "preact-compat",
|
||||||
|
"react-dom": "preact-compat"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"syntax-async-functions",
|
||||||
|
"transform-async-to-generator",
|
||||||
|
"syntax-trailing-function-commas"
|
||||||
|
],
|
||||||
|
"sourceMaps": true
|
||||||
|
}
|
|
@ -9,10 +9,36 @@ jobs:
|
||||||
- checkout
|
- checkout
|
||||||
- setup_docker_engine
|
- setup_docker_engine
|
||||||
|
|
||||||
- run:
|
# Enable caching: https://circleci.com/blog/how-to-build-a-docker-image-on-circleci-2-0/
|
||||||
name: Build Docker images
|
- restore_cache:
|
||||||
command: make build
|
keys:
|
||||||
|
- v1-{{ .Branch }}
|
||||||
|
paths:
|
||||||
|
- /caches/rookeries-app.tar
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Load Docker image layer cache
|
||||||
|
command: |
|
||||||
|
set +o pipefail
|
||||||
|
docker load --input /caches/rookeries-app.tar | true
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Build Rookeries Docker image
|
||||||
|
command: |
|
||||||
|
docker build --cache-from=dorianpula/rookeries:latest --tag dorianpula/rookeries:latest .
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Save Docker image layer cache
|
||||||
|
command: |
|
||||||
|
mkdir -p /caches
|
||||||
|
docker save --output /caches/rookeries-app.tar dorianpula/rookeries:latest
|
||||||
|
|
||||||
|
- save_cache:
|
||||||
|
key: v1-{{ .Branch }}-{{ epoch }}
|
||||||
|
paths:
|
||||||
|
- /caches/rookeries-app.tar
|
||||||
|
|
||||||
|
# Run tests
|
||||||
- run:
|
- run:
|
||||||
name: Test API
|
name: Test API
|
||||||
command: make test-api
|
command: make test-api
|
||||||
|
|
|
@ -6,11 +6,16 @@
|
||||||
"react",
|
"react",
|
||||||
"import"
|
"import"
|
||||||
],
|
],
|
||||||
"ecmaFeatures": {
|
|
||||||
"modules": true,
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"parser": "babel-eslint",
|
"parser": "babel-eslint",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 7,
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"modules": true,
|
||||||
|
"jsx": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"arrow-body-style": [2, "always"],
|
"arrow-body-style": [2, "always"],
|
||||||
"arrow-parens": 2,
|
"arrow-parens": 2,
|
||||||
|
@ -21,6 +26,7 @@
|
||||||
"default-case": 1,
|
"default-case": 1,
|
||||||
"dot-notation": 2,
|
"dot-notation": 2,
|
||||||
"eqeqeq": 2,
|
"eqeqeq": 2,
|
||||||
|
"indent": [1, 2],
|
||||||
"jsx-quotes": 1,
|
"jsx-quotes": 1,
|
||||||
"no-console": 0,
|
"no-console": 0,
|
||||||
"no-alert": 2,
|
"no-alert": 2,
|
||||||
|
@ -54,14 +60,14 @@
|
||||||
"react/forbid-prop-types": 1,
|
"react/forbid-prop-types": 1,
|
||||||
"react/jsx-boolean-value": 1,
|
"react/jsx-boolean-value": 1,
|
||||||
"react/jsx-closing-bracket-location": [1, {"selfClosing": "after-props"}],
|
"react/jsx-closing-bracket-location": [1, {"selfClosing": "after-props"}],
|
||||||
"react/jsx-curly-spacing": [2, "always", {"allowMultiline": false}],
|
"react/jsx-curly-spacing": [2, {"when": "always", "allowMultiline": false, "spacing": {"objectLiterals": "never"}}],
|
||||||
|
"react/jsx-filename-extension": 0,
|
||||||
"react/jsx-equals-spacing": 1,
|
"react/jsx-equals-spacing": 1,
|
||||||
"react/jsx-handler-names": 1,
|
"react/jsx-handler-names": 1,
|
||||||
"react/jsx-indent-props": 1,
|
"react/jsx-indent-props": [1, 2],
|
||||||
"react/jsx-indent": 1,
|
"react/jsx-indent": [1, 2],
|
||||||
"react/jsx-key": 1,
|
"react/jsx-key": 1,
|
||||||
"react/jsx-max-props-per-line": 0,
|
"react/jsx-max-props-per-line": 0,
|
||||||
"react/jsx-wrap-multilines": 1,
|
|
||||||
"react/jsx-no-bind": 1,
|
"react/jsx-no-bind": 1,
|
||||||
"react/jsx-no-duplicate-props": 1,
|
"react/jsx-no-duplicate-props": 1,
|
||||||
"react/jsx-no-literals": 0,
|
"react/jsx-no-literals": 0,
|
||||||
|
@ -70,6 +76,7 @@
|
||||||
"react/jsx-sort-props": 0,
|
"react/jsx-sort-props": 0,
|
||||||
"react/jsx-uses-react": 1,
|
"react/jsx-uses-react": 1,
|
||||||
"react/jsx-uses-vars": 1,
|
"react/jsx-uses-vars": 1,
|
||||||
|
"react/jsx-wrap-multilines": 1,
|
||||||
"react/no-danger": 0,
|
"react/no-danger": 0,
|
||||||
"react/no-deprecated": 1,
|
"react/no-deprecated": 1,
|
||||||
"react/no-did-mount-set-state": 1,
|
"react/no-did-mount-set-state": 1,
|
||||||
|
@ -87,6 +94,7 @@
|
||||||
"react/sort-comp": 1,
|
"react/sort-comp": 1,
|
||||||
"react/sort-prop-types": 1
|
"react/sort-prop-types": 1
|
||||||
},
|
},
|
||||||
|
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"node": true,
|
"node": true,
|
||||||
|
|
18
Dockerfile
18
Dockerfile
|
@ -1,9 +1,13 @@
|
||||||
FROM python:3.6
|
FROM python:3.6-slim
|
||||||
MAINTAINER Dorian Pula <dorian.pula@amber-penguin-software.ca>
|
MAINTAINER Dorian Pula <dorian.pula@amber-penguin-software.ca>
|
||||||
|
|
||||||
# Add Docker utilities
|
# Add Docker utilities
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y wget curl gcc
|
||||||
|
|
||||||
RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64.deb \
|
RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64.deb \
|
||||||
&& dpkg -i dumb-init_*.deb
|
&& dpkg -i dumb-init_*.deb \
|
||||||
|
&& rm dumb-init_*.deb
|
||||||
|
|
||||||
ENV DOCKERIZE_VERSION v0.3.0
|
ENV DOCKERIZE_VERSION v0.3.0
|
||||||
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
|
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
|
||||||
|
@ -11,21 +15,19 @@ RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSI
|
||||||
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
|
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
|
||||||
|
|
||||||
# Install Node and Yarn
|
# Install Node and Yarn
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - \
|
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - \
|
||||||
&& apt-get install -y nodejs \
|
|
||||||
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||||
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
|
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y yarn
|
&& apt-get install -y nodejs yarn
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies
|
||||||
COPY requirements.txt /app/rookeries/
|
COPY requirements.txt /app/rookeries/
|
||||||
RUN pip install --requirement /app/rookeries/requirements.txt
|
RUN pip install --requirement /app/rookeries/requirements.txt
|
||||||
|
|
||||||
# Install Node dependencies
|
# Install Node dependencies
|
||||||
COPY package.json /app/rookeries/
|
COPY ["package.json", "yarn.lock", "/app/rookeries/"]
|
||||||
COPY yarn.lock /app/rookeries/
|
RUN cd /app/rookeries && yarn install --silent
|
||||||
RUN cd /app/rookeries && yarn install
|
|
||||||
|
|
||||||
# Copy in Rookeries server code
|
# Copy in Rookeries server code
|
||||||
COPY . /app/rookeries/
|
COPY . /app/rookeries/
|
||||||
|
|
32
Makefile
32
Makefile
|
@ -6,29 +6,37 @@ build-git-tag:
|
||||||
|
|
||||||
# Build the API image
|
# Build the API image
|
||||||
build: build-git-tag
|
build: build-git-tag
|
||||||
docker-compose build --pull rookeries
|
docker build \
|
||||||
|
--pull \
|
||||||
|
--cache-from=dorianpula/rookeries:latest \
|
||||||
|
--tag dorianpula/rookeries:latest .
|
||||||
|
|
||||||
# Runs all the tests
|
# Runs all the tests
|
||||||
test: build test-api test-ui
|
test: build test-api test-ui
|
||||||
|
|
||||||
# Runs API tests
|
# Runs API tests
|
||||||
test-api: stop build
|
test-api: stop build
|
||||||
docker-compose run --no-deps rookeries inv test.python_style
|
docker-compose run --no-deps rookeries \
|
||||||
|
inv test.python_style
|
||||||
docker-compose up -d db rookeries
|
docker-compose up -d db rookeries
|
||||||
docker-compose exec rookeries inv db.wait
|
docker-compose exec rookeries \
|
||||||
docker-compose exec rookeries inv db.init
|
inv db.wait db.init db.upgrade
|
||||||
docker-compose exec rookeries inv db.upgrade
|
docker-compose exec rookeries \
|
||||||
docker-compose exec rookeries inv server.wait --timeout 30
|
inv server.wait --timeout 100
|
||||||
docker-compose exec rookeries inv test.server
|
docker-compose exec rookeries \
|
||||||
|
inv test.server
|
||||||
|
|
||||||
# Runs UI tests
|
# Runs UI tests
|
||||||
test-ui: stop build
|
test-ui: stop build
|
||||||
docker-compose run --no-deps rookeries inv test.js_style
|
docker-compose run --no-deps rookeries \
|
||||||
docker-compose run --no-deps rookeries inv test.js
|
inv test.js_style test.js
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
docker-compose exec rookeries inv db.wait
|
docker-compose exec rookeries \
|
||||||
docker-compose exec rookeries inv server.wait --timeout 100
|
inv db.wait
|
||||||
docker-compose exec rookeries inv test.ui
|
docker-compose exec rookeries \
|
||||||
|
inv server.wait --timeout 100
|
||||||
|
docker-compose exec rookeries \
|
||||||
|
inv test.ui
|
||||||
|
|
||||||
# Demos Rookeries in a browser
|
# Demos Rookeries in a browser
|
||||||
demo: build
|
demo: build
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
[[source]]
|
||||||
|
url = "https://pypi.python.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
|
||||||
|
Flask = "==0.11.1"
|
||||||
|
flask-appconfig = "==0.11.1"
|
||||||
|
Flask-JWT = "==0.3.2"
|
||||||
|
PyJWT = ">=1.4.0"
|
||||||
|
SQLAlchemy = "==1.1.6"
|
||||||
|
SQLAlchemy-Utils = "==0.32.12"
|
||||||
|
Flask-SQLAlchemy = "==2.2"
|
||||||
|
psycopg2 = "==2.7.1"
|
||||||
|
flask-alembic = "==2.0.1"
|
||||||
|
arrow = "==0.10.0"
|
||||||
|
jsonschema = "==2.6.0"
|
||||||
|
requests = "==2.13.0"
|
||||||
|
bpython = ">=0.15"
|
||||||
|
Sphinx = ">=1.4.8"
|
||||||
|
releases = ">=1.2.1"
|
||||||
|
recommonmark = ">=0.4.0"
|
||||||
|
flake8 = ">=3.0.4"
|
||||||
|
pyflakes = "==1.5.0"
|
||||||
|
pytest = ">=3.0.3"
|
||||||
|
pytest-mock = ">=1.2"
|
||||||
|
pytest-bdd = ">=2.18.0"
|
||||||
|
pytest-random = "==0.02"
|
||||||
|
pytest-splinter = ">=1.8.0"
|
||||||
|
invoke = "==0.13.0"
|
||||||
|
uwsgi = "==2.0.14"
|
||||||
|
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.6"
|
|
@ -0,0 +1,505 @@
|
||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "34222c6fe02c50dbbb703804972ca0c659fbe248b1528164f6e90a5bfc359c15"
|
||||||
|
},
|
||||||
|
"host-environment-markers": {
|
||||||
|
"implementation_name": "cpython",
|
||||||
|
"implementation_version": "3.6.2",
|
||||||
|
"os_name": "posix",
|
||||||
|
"platform_machine": "x86_64",
|
||||||
|
"platform_python_implementation": "CPython",
|
||||||
|
"platform_release": "4.4.0-93-generic",
|
||||||
|
"platform_system": "Linux",
|
||||||
|
"platform_version": "#116-Ubuntu SMP Fri Aug 11 21:17:51 UTC 2017",
|
||||||
|
"python_full_version": "3.6.2",
|
||||||
|
"python_version": "3.6",
|
||||||
|
"sys_platform": "linux"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 4,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.6"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"url": "https://pypi.python.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"alabaster": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2eef172f44e8d301d25aff8068fddd65f767a3f04b5f15b0f4922f113aa1c732",
|
||||||
|
"sha256:37cdcb9e9954ed60912ebc1ca12a9d12178c26637abdf124e3cde2341c257fe0"
|
||||||
|
],
|
||||||
|
"version": "==0.7.10"
|
||||||
|
},
|
||||||
|
"alembic": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8bdcb4babaa16b9a826f8084949cc2665cb328ecf7b89b3224b0ab85bd16fd05"
|
||||||
|
],
|
||||||
|
"version": "==0.9.5"
|
||||||
|
},
|
||||||
|
"arrow": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:805906f09445afc1f0fc80187db8fe07670e3b25cdafa09b8d8ac264a8c0c722"
|
||||||
|
],
|
||||||
|
"version": "==0.10.0"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f20b2acd44f587988ff185d8949c3e208b4b3d5d20fcab7d91fe481ffa435528",
|
||||||
|
"sha256:6007daf714d0cd5524bbe436e2d42b3c20e68da66289559341e48d2cd6d25811"
|
||||||
|
],
|
||||||
|
"version": "==2.5.1"
|
||||||
|
},
|
||||||
|
"blessings": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:edc5713061f10966048bf6b40d9a514b381e0ba849c64e034c4ef6c1847d3007"
|
||||||
|
],
|
||||||
|
"version": "==1.6"
|
||||||
|
},
|
||||||
|
"bpython": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2e1466059cafbbcfd232e8e7ead821daf520bc7b2ccb02bca6a98ff8b0ef18d9",
|
||||||
|
"sha256:faf3ddf602bd8ad7f133011778966333b9dcefbc3100df27a200b648906f655f"
|
||||||
|
],
|
||||||
|
"version": "==0.16"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
|
||||||
|
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
|
||||||
|
],
|
||||||
|
"version": "==6.7"
|
||||||
|
},
|
||||||
|
"commonmark": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:34d73ec8085923c023930dfc0bcd1c4286e28a2a82de094bb72fabcc0281cbe5"
|
||||||
|
],
|
||||||
|
"version": "==0.5.4"
|
||||||
|
},
|
||||||
|
"configparser": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5308b47021bc2340965c371f0f058cc6971a04502638d4244225c49d80db273a"
|
||||||
|
],
|
||||||
|
"version": "==3.5.0"
|
||||||
|
},
|
||||||
|
"curtsies": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8af7b0df83270a8d53c972b3436b7ac7813ffca27e6301c53b788372e3d862c2",
|
||||||
|
"sha256:ec4639df654357944c201abd0d791d9dfd3ac27ce4f90ba1c2991aaf07af92ee"
|
||||||
|
],
|
||||||
|
"version": "==0.2.11"
|
||||||
|
},
|
||||||
|
"docutils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6",
|
||||||
|
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
||||||
|
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274"
|
||||||
|
],
|
||||||
|
"version": "==0.14"
|
||||||
|
},
|
||||||
|
"enum34": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79",
|
||||||
|
"sha256:644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a",
|
||||||
|
"sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1",
|
||||||
|
"sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850"
|
||||||
|
],
|
||||||
|
"version": "==1.1.6"
|
||||||
|
},
|
||||||
|
"flake8": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f1a9d8886a9cbefb52485f4f4c770832c7fb569c084a9a314fb1eaa37c0c2c86",
|
||||||
|
"sha256:c20044779ff848f67f89c56a0e4624c04298cd476e25253ac0c36f910a1a11d8"
|
||||||
|
],
|
||||||
|
"version": "==3.4.1"
|
||||||
|
},
|
||||||
|
"flask": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a4f97abd30d289e548434ef42317a793f58087be1989eab96f2c647470e77000",
|
||||||
|
"sha256:b4713f2bfb9ebc2966b8a49903ae0d3984781d5c878591cf2f7b484d28756b0e"
|
||||||
|
],
|
||||||
|
"version": "==0.11.1"
|
||||||
|
},
|
||||||
|
"flask-alembic": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7e67740b0b08d58dcae0c701d56b56e60f5fa4af907bb82b4cb0469229ba94ff",
|
||||||
|
"sha256:05a1e6f4148dbfcc9280a393373bfbd250af6f9f4f0ca9f744ef8f7376a3deec"
|
||||||
|
],
|
||||||
|
"version": "==2.0.1"
|
||||||
|
},
|
||||||
|
"flask-appconfig": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:360d0598d48bdf3f421608d1181aa96183fc09f59f24fe426e1e8e372ed42647"
|
||||||
|
],
|
||||||
|
"version": "==0.11.1"
|
||||||
|
},
|
||||||
|
"flask-jwt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:49c0672fbde0f1cd3374bd834918d28956e3c521c7e00089cdc5380d323bd0ad"
|
||||||
|
],
|
||||||
|
"version": "==0.3.2"
|
||||||
|
},
|
||||||
|
"flask-sqlalchemy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:fa3bd6343de231b2b3376a276df6d331c63c766449b660fc04d375a1c21312ac",
|
||||||
|
"sha256:f0d8241efba723d7b878f73550f5d3c0fbb042416123b52b36640b7491fa208b"
|
||||||
|
],
|
||||||
|
"version": "==2.2"
|
||||||
|
},
|
||||||
|
"funcsigs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
|
||||||
|
"sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
|
||||||
|
],
|
||||||
|
"version": "==1.0.2"
|
||||||
|
},
|
||||||
|
"functools32": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f6253dfbe0538ad2e387bd8fdfd9293c925d63553f5813c4e587745416501e6d",
|
||||||
|
"sha256:89d824aa6c358c421a234d7f9ee0bd75933a67c29588ce50aaa3acdf4d403fa0"
|
||||||
|
],
|
||||||
|
"version": "==3.2.3.post2"
|
||||||
|
},
|
||||||
|
"glob2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f5b0a686ff21f820c4d3f0c4edd216704cea59d79d00fa337e244a2f2ff83ed6"
|
||||||
|
],
|
||||||
|
"version": "==0.6"
|
||||||
|
},
|
||||||
|
"greenlet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:96888e47898a471073b394ea641b7d675c1d054c580dd4a04a382bd34e67d89e",
|
||||||
|
"sha256:d2d5103f6cba131e1be660230018e21f276911d2b68b629ead1c5cb5e5472ac7",
|
||||||
|
"sha256:bc339de0e0969de5118d0b62a080a7611e2ba729a90f4a3ad78559c51bc5576d",
|
||||||
|
"sha256:b8ab98f8ae25938326dc4c21e3689a933531500ae4f3bfcefe36e3e25fda4dbf",
|
||||||
|
"sha256:416a3328d7e0a19aa1df3ec09524a109061fd7b80e010ef0dff9f695b4ac5e20",
|
||||||
|
"sha256:21232907c8c26838b16915bd8fbbf82fc70c996073464cc70981dd4a96bc841c",
|
||||||
|
"sha256:6803d8c6b235c861c50afddf00c7467ffbcd5ab960d137ff0f9c36f2cb11ee4b",
|
||||||
|
"sha256:76dab055476dd4dabb00a967b4df1990b25542d17eaa40a18f66971d10193e0b",
|
||||||
|
"sha256:70b9ff28921f5a3c03df4896ec8c55f5f94c593d7a79abd98b4c5c4a692ba873",
|
||||||
|
"sha256:7114b757b4146f4c87a0f00f1e58abd4c4729836679af0fc37266910a4a72eb0",
|
||||||
|
"sha256:0d90c709355ed13f16676f84e5a9cd67826a9f5c5143381c21e8fc3100ade1f1",
|
||||||
|
"sha256:ebae83b6247f83b1e8d887733dfa8046ce6e29d8b3e2a7380256e9de5c6ae55d",
|
||||||
|
"sha256:e841e3ece633acae5e2bf6102140a605ffee7d5d4921dca1625c5fdc0f0b3248",
|
||||||
|
"sha256:3e5e9be157ece49e4f97f3225460caf758ccb00f934fcbc5db34367cc1ff0aee",
|
||||||
|
"sha256:e77b708c37b652c7501b9f8f6056b23633c567aaa0d29edfef1c11673c64b949",
|
||||||
|
"sha256:0da1fc809c3bdb93fbacd0f921f461aacd53e554a7b7d4e9953ba09131c4206e",
|
||||||
|
"sha256:66fa5b101fcf4521138c1a29668074268d938bbb7de739c8faa9f92ea1f05e1f",
|
||||||
|
"sha256:e5451e1ce06b74a4861576c2db74405a4398c4809a105774550a9e52cfc8c4da",
|
||||||
|
"sha256:9c407aa6adfd4eea1232e81aa9f3cb3d9b955a9891c4819bf9b498c77efba14b",
|
||||||
|
"sha256:b56ac981f07b77e72ad5154278b93396d706572ea52c2fce79fee2abfcc8bfa6",
|
||||||
|
"sha256:e4c99c6010a5d153d481fdaf63b8a0782825c0721506d880403a3b9b82ae347e"
|
||||||
|
],
|
||||||
|
"version": "==0.4.12"
|
||||||
|
},
|
||||||
|
"imagesize": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6ebdc9e0ad188f9d1b2cdd9bc59cbe42bf931875e829e7a595e6b3abdc05cdfb",
|
||||||
|
"sha256:0ab2c62b87987e3252f89d30b7cedbec12a01af9274af9ffa48108f2c13c6062"
|
||||||
|
],
|
||||||
|
"version": "==0.7.1"
|
||||||
|
},
|
||||||
|
"invoke": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1ffaaba734fb5419d2f58b3ccf6c51d6cbb0c0bc9ba24c69f6a8b8c3a93b323a",
|
||||||
|
"sha256:ad6aaeb46bc17c3c0542935138b46b82c2486f65709eb27315ff8f4ab9b2b3e7",
|
||||||
|
"sha256:1a1992fac5292b97448d1c85dc0793e309c4c376acbc39ff067056d71fdc241d"
|
||||||
|
],
|
||||||
|
"version": "==0.13.0"
|
||||||
|
},
|
||||||
|
"itsdangerous": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
|
||||||
|
],
|
||||||
|
"version": "==0.24"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2231bace0dfd8d2bf1e5d7e41239c06c9e0ded46e70cc1094a0aa64b0afeb054",
|
||||||
|
"sha256:ddaa01a212cd6d641401cb01b605f4a4d9f37bfc93043d7f760ec70fb99ff9ff"
|
||||||
|
],
|
||||||
|
"version": "==2.9.6"
|
||||||
|
},
|
||||||
|
"jsonschema": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08",
|
||||||
|
"sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"
|
||||||
|
],
|
||||||
|
"version": "==2.6.0"
|
||||||
|
},
|
||||||
|
"mako": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae"
|
||||||
|
],
|
||||||
|
"version": "==1.0.7"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
|
||||||
|
],
|
||||||
|
"version": "==1.0"
|
||||||
|
},
|
||||||
|
"mccabe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||||
|
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||||
|
],
|
||||||
|
"version": "==0.6.1"
|
||||||
|
},
|
||||||
|
"mock": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1",
|
||||||
|
"sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"
|
||||||
|
],
|
||||||
|
"version": "==2.0.0"
|
||||||
|
},
|
||||||
|
"parse": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8048dde3f5ca07ad7ac7350460952d83b63eaacecdac1b37f45fd74870d849d2"
|
||||||
|
],
|
||||||
|
"version": "==1.8.2"
|
||||||
|
},
|
||||||
|
"parse-type": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3dd0b323bafcb8c25e000ce5589042a1c99cba9c3bec77b9f591e46bc9606147"
|
||||||
|
],
|
||||||
|
"version": "==0.3.4"
|
||||||
|
},
|
||||||
|
"pbr": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:60c25b7dfd054ef9bb0ae327af949dd4676aa09ac3a9471cdc871d8a9213f9ac",
|
||||||
|
"sha256:05f61c71aaefc02d8e37c0a3eeb9815ff526ea28b3b76324769e6158d7f95be1"
|
||||||
|
],
|
||||||
|
"version": "==3.1.1"
|
||||||
|
},
|
||||||
|
"psycopg2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f4cc27830081147ebca6eb729329c64b7f8e92e415733aa26705dd9f03303038",
|
||||||
|
"sha256:34b7e41d1747b26df3baf5397949cc938261c547f3bce208ffc7826c3897d5ac",
|
||||||
|
"sha256:2a9c4bc78922be9d34a11fc38b75bef122d44f465eaf5f2a44021f7cbda3dfd6",
|
||||||
|
"sha256:9f9de8b5e1b0b648bf4535b721a58eaccf92f747cd8d94380171a36f18553601",
|
||||||
|
"sha256:0c68fb507415ce022402244cffe740018c9506367fe2d6323f0b47e2544b6b36",
|
||||||
|
"sha256:0147e990cef0332e239de711f165b2a5e6feb7c5f8583985d4a3dcac1680a782",
|
||||||
|
"sha256:f1f6d1b0ab40e8d43e294c7a1cc6fe6d47a88225583e581c15e8bfcff04b3863",
|
||||||
|
"sha256:18c2294c2a0a0df6bb5adfbcae50922b0e75909df8642c9286bf6b867866d181",
|
||||||
|
"sha256:bf48be2d01f6e34e5fbd8976f7b66158a83a33b92739acd13fc1934c60b71cfb",
|
||||||
|
"sha256:d2e98ff1546c632c29f740a938ba6e29d327ef55f3e816dd43c1e4e7d8cd369a",
|
||||||
|
"sha256:4004e84c2846badfa1e14ba3cf5b9d7471071c872c1dd6290013c89fa2cc63b1",
|
||||||
|
"sha256:26551d8873130b9558628c5f0288bd07b00c3f82f7dbfe0acf3cd7cd45746672",
|
||||||
|
"sha256:304304e8efce4077621f9a14cb7e53cdacb4a14ec92f5628204bc4254d4840fb",
|
||||||
|
"sha256:6b26f9ec7a0c5ca1b3528918caeb3c73bfceaced3fdea59d045c886cc2c43344",
|
||||||
|
"sha256:a14bd664f4db452612688e8031fdf74da489cb042d78d49f2bbfcfa97aae135d",
|
||||||
|
"sha256:ad0e145952e2e91aa1d80c4b9bd8ba9059f39b3538da7b1133b2ed84eb132ed3",
|
||||||
|
"sha256:905764e8d1557e536c9fac35d412a21ad87b558e794104437bf4012a83438ed2",
|
||||||
|
"sha256:f04d56f664108af9571d278c0ed942ecef0dd3e072fe21ac8c7497a51bfcdfba",
|
||||||
|
"sha256:3a11a757e61e3eb5dfa334cf0ce86f9fa1919efadbb98af833eaf3bcb90d7e58",
|
||||||
|
"sha256:ccd55766033411206d700bcd11e064081f784f256c4d4f6883dca4c3bde21ae2",
|
||||||
|
"sha256:38ff8f65deb947a7499f24ceb5b40db24540027fdf7d927f4d2aab35fbbdd6b0",
|
||||||
|
"sha256:3f64897312a08928e71343fedc2a3812a1c46f03025fe327e5d41e0a7be53ee9",
|
||||||
|
"sha256:9c9fe11f87c44e83620b2273f40338e3c25fdbfd34986dddc77e840e0acd25bc",
|
||||||
|
"sha256:32184c60b445ac9eb4a0058d54f7a5dddae6928f94dfa3683157dc5e70321212",
|
||||||
|
"sha256:b4f24faa25232073092d246fc6118c29130c8a5bcc8fd4239edeba20fb12b24a",
|
||||||
|
"sha256:74ee532d6068a1456d5eda254513656c716cdee0a7389c91c094c7ce3500a9c9",
|
||||||
|
"sha256:ba6f4786262b4999f9cca420827469c88a79e1f65dabf6bb8a300518f7e11472",
|
||||||
|
"sha256:4cae3856e9b41a4861ad32fd8a4aabc7669a0b828776bb3ce0ca29333dabf40c",
|
||||||
|
"sha256:8f57535a377c23a78d5a8bbe929ec6977adb3e137fa42500436c784c60141613",
|
||||||
|
"sha256:911af115bb526b3a5d2f50e05096afc05e3867f55f24532b4a70811e63deab57",
|
||||||
|
"sha256:86c9355f5374b008c8479bc00023b295c07d508f7c3b91dbd2e74f8925b1d9c6"
|
||||||
|
],
|
||||||
|
"version": "==2.7.1"
|
||||||
|
},
|
||||||
|
"py": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a",
|
||||||
|
"sha256:0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3"
|
||||||
|
],
|
||||||
|
"version": "==1.4.34"
|
||||||
|
},
|
||||||
|
"pycodestyle": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9",
|
||||||
|
"sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766"
|
||||||
|
],
|
||||||
|
"version": "==2.3.1"
|
||||||
|
},
|
||||||
|
"pyflakes": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:cc5eadfb38041f8366128786b4ca12700ed05bbf1403d808e89d57d67a3875a7",
|
||||||
|
"sha256:aa0d4dff45c0cc2214ba158d29280f8fa1129f3e87858ef825930845146337f4"
|
||||||
|
],
|
||||||
|
"version": "==1.5.0"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
|
||||||
|
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
|
||||||
|
],
|
||||||
|
"version": "==2.2.0"
|
||||||
|
},
|
||||||
|
"pyjwt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:99fe612dbe5f41e07124d9002c118c14f3ee703574ffa9779fee78135b8b94b6",
|
||||||
|
"sha256:87a831b7a3bfa8351511961469ed0462a769724d4da48a501cb8c96d1e17f570"
|
||||||
|
],
|
||||||
|
"version": "==1.4.2"
|
||||||
|
},
|
||||||
|
"pytest": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b84f554f8ddc23add65c411bf112b2d88e2489fd45f753b1cae5936358bdf314",
|
||||||
|
"sha256:f46e49e0340a532764991c498244a60e3a37d7424a532b3ff1a6a7653f1a403a"
|
||||||
|
],
|
||||||
|
"version": "==3.2.2"
|
||||||
|
},
|
||||||
|
"pytest-bdd": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b63ca3d214d62099e9e13cbbfee91f6d283a3a2e1d6c5730ea083c3caae6a326"
|
||||||
|
],
|
||||||
|
"version": "==2.18.2"
|
||||||
|
},
|
||||||
|
"pytest-mock": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7ed6e7e8c636fd320927c5d73aedb77ac2eeb37196c3410e6176b7c92fdf2f69",
|
||||||
|
"sha256:920d1167af5c2c2ad3fa0717d0c6c52e97e97810160c15721ac895cac53abb1c"
|
||||||
|
],
|
||||||
|
"version": "==1.6.3"
|
||||||
|
},
|
||||||
|
"pytest-random": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:92f25db8c5d9ffc20d90b51997b914372d6955cb9cf1f6ead45b90514fc0eddd"
|
||||||
|
],
|
||||||
|
"version": "==0.2"
|
||||||
|
},
|
||||||
|
"pytest-splinter": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3435a97e161532c947624583369177cff6cd2c39e017ee3493ca9b95cb0375ee"
|
||||||
|
],
|
||||||
|
"version": "==1.8.5"
|
||||||
|
},
|
||||||
|
"python-dateutil": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:95511bae634d69bc7329ba55e646499a842bc4ec342ad54a8cdb65645a0aad3c",
|
||||||
|
"sha256:891c38b2a02f5bb1be3e4793866c8df49c7d19baabf9c1bad62547e0b4866aca"
|
||||||
|
],
|
||||||
|
"version": "==2.6.1"
|
||||||
|
},
|
||||||
|
"python-editor": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a3c066acee22a1c94f63938341d4fb374e3fdd69366ed6603d7b24bed1efc565"
|
||||||
|
],
|
||||||
|
"version": "==1.0.3"
|
||||||
|
},
|
||||||
|
"pytz": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c883c2d6670042c7bc1688645cac73dd2b03193d1f7a6847b6154e96890be06d",
|
||||||
|
"sha256:03c9962afe00e503e2d96abab4e8998a0f84d4230fa57afe1e0528473698cdd9",
|
||||||
|
"sha256:487e7d50710661116325747a9cd1744d3323f8e49748e287bc9e659060ec6bf9",
|
||||||
|
"sha256:43f52d4c6a0be301d53ebd867de05e2926c35728b3260157d274635a0a947f1c",
|
||||||
|
"sha256:d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67",
|
||||||
|
"sha256:54a935085f7bf101f86b2aff75bd9672b435f51c3339db2ff616e66845f2b8f9",
|
||||||
|
"sha256:39504670abb5dae77f56f8eb63823937ce727d7cdd0088e6909e6dcac0f89043",
|
||||||
|
"sha256:ddc93b6d41cfb81266a27d23a79e13805d4a5521032b512643af8729041a81b4",
|
||||||
|
"sha256:f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589"
|
||||||
|
],
|
||||||
|
"version": "==2017.2"
|
||||||
|
},
|
||||||
|
"recommonmark": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:cd8bf902e469dae94d00367a8197fb7b81fcabc9cfb79d520e0d22d0fbeaa8b7",
|
||||||
|
"sha256:6e29c723abcf5533842376d87c4589e62923ecb6002a8e059eb608345ddaff9d"
|
||||||
|
],
|
||||||
|
"version": "==0.4.0"
|
||||||
|
},
|
||||||
|
"releases": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9f35944b7f32a4b84557df1064c2f51339388b044aa78ee9f57917f8ac990cd3",
|
||||||
|
"sha256:b3131b028308108da423d9c7d5877be63be17e645bde6d5dfddadc49f7b0932c"
|
||||||
|
],
|
||||||
|
"version": "==1.3.1"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1a720e8862a41aa22e339373b526f508ef0c8988baf48b84d3fc891a8e237efb",
|
||||||
|
"sha256:5722cd09762faa01276230270ff16af7acf7c5c45d623868d9ba116f15791ce8"
|
||||||
|
],
|
||||||
|
"version": "==2.13.0"
|
||||||
|
},
|
||||||
|
"selenium": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:69b479bdfa1ab2fee86a75086f7d5c6ef93cdad8cb6521cb0596554c6722f3eb",
|
||||||
|
"sha256:267418f5fde1a4f8c180e5b8f45bd57c6d45b1f7d8fa5ad699a48e9a98fa79a3"
|
||||||
|
],
|
||||||
|
"version": "==3.5.0"
|
||||||
|
},
|
||||||
|
"semantic-version": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2d06ab7372034bcb8b54f2205370f4aa0643c133b7e6dbd129c5200b83ab394b",
|
||||||
|
"sha256:2a4328680073e9b243667b201119772aefc5fc63ae32398d6afafff07c4f54c0"
|
||||||
|
],
|
||||||
|
"version": "==2.6.0"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb",
|
||||||
|
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9"
|
||||||
|
],
|
||||||
|
"version": "==1.11.0"
|
||||||
|
},
|
||||||
|
"snowballstemmer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89",
|
||||||
|
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128"
|
||||||
|
],
|
||||||
|
"version": "==1.2.1"
|
||||||
|
},
|
||||||
|
"sphinx": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b83f430200f546bfd5088c653f0c5516af708da36066dfde08d08bedb1b33a4b",
|
||||||
|
"sha256:82cd2728c906be96e307b81352d3fd9fb731869234c6b835cc25e9a3dfb4b7e4"
|
||||||
|
],
|
||||||
|
"version": "==1.4.9"
|
||||||
|
},
|
||||||
|
"splinter": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f25095ee8f3884054835c6b1fd58f3a6ee58942c411ab41238fb2471196f46f9"
|
||||||
|
],
|
||||||
|
"version": "==0.7.6"
|
||||||
|
},
|
||||||
|
"sqlalchemy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:815924e3218d878ddd195d2f9f5bf3d2bb39fabaddb1ea27dace6ac27d9865e4"
|
||||||
|
],
|
||||||
|
"version": "==1.1.6"
|
||||||
|
},
|
||||||
|
"sqlalchemy-utils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:cc8d339611939a4f4d32d428a4e81f484c13525dc38aa21e93a3f1ecbba5be98"
|
||||||
|
],
|
||||||
|
"version": "==0.32.12"
|
||||||
|
},
|
||||||
|
"uwsgi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:21b3d1ef926d835ff23576193a2c60d4c896d8e21567850cf0677a4764122887"
|
||||||
|
],
|
||||||
|
"version": "==2.0.14"
|
||||||
|
},
|
||||||
|
"wcwidth": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c",
|
||||||
|
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e"
|
||||||
|
],
|
||||||
|
"version": "==0.1.7"
|
||||||
|
},
|
||||||
|
"werkzeug": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:e8549c143af3ce6559699a01e26fa4174f4c591dbee0a499f3cd4c3781cdec3d",
|
||||||
|
"sha256:903a7b87b74635244548b30d30db4c8947fe64c5198f58899ddcd3a13c23bb26"
|
||||||
|
],
|
||||||
|
"version": "==0.12.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {}
|
||||||
|
}
|
|
@ -4,8 +4,6 @@ services:
|
||||||
rookeries:
|
rookeries:
|
||||||
build: .
|
build: .
|
||||||
image: dorianpula/rookeries:latest
|
image: dorianpula/rookeries:latest
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
environment:
|
environment:
|
||||||
- ROOKERIES_SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://admin:password@db:5432/rookeries
|
- ROOKERIES_SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://admin:password@db:5432/rookeries
|
||||||
- ROOKERIES_JWT_SECRET_KEY=problematic_penguins
|
- ROOKERIES_JWT_SECRET_KEY=problematic_penguins
|
||||||
|
@ -20,8 +18,6 @@ services:
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:9.6
|
image: postgres:9.6
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_USER=admin
|
- POSTGRES_USER=admin
|
||||||
- POSTGRES_PASSWORD=password
|
- POSTGRES_PASSWORD=password
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
- Sports a flexible layout engine, and intuitive editor.
|
- Sports a flexible layout engine, and intuitive editor.
|
||||||
- Supports multiple themes and user personalization.
|
- Supports multiple themes and user personalization.
|
||||||
- Powered by a modern Python server side using Flask and CouchDB.
|
- Powered by a modern Python server side using Flask and CouchDB.
|
||||||
- Uses React, Redux, Bootstrap and LESS to create a gorgeous frontend experience.
|
- Uses preact, redux, bootstrap and webpack to create a gorgeous frontend experience.
|
||||||
- Licensed freely under the Affero GNU Genearal Public License (AGPL) version 3.0.
|
- Licensed freely under the Affero GNU Genearal Public License (AGPL) version 3.0.
|
||||||
|
|
||||||
## Powered By
|
## Powered By
|
||||||
|
@ -24,5 +24,5 @@ below technologies (and many more). Thank you!!!
|
||||||
|
|
||||||
| Language | JS Frameworks | CSS Frameworks | Code Editor |
|
| Language | JS Frameworks | CSS Frameworks | Code Editor |
|
||||||
| :---: | :---: | :---: | :---: |
|
| :---: | :---: | :---: | :---: |
|
||||||
| <img src="https://raw.githubusercontent.com/voodootikigod/logo.js/master/js.png" alt="JS" width=100 /> | <img alt="React" src="https://facebook.github.io/react/img/logo.svg" width=100 /> <img alt="Redux" src="https://camo.githubusercontent.com/f28b5bc7822f1b7bb28a96d8d09e7d79169248fc/687474703a2f2f692e696d6775722e636f6d2f4a65567164514d2e706e67" width=200 /> |  <img alt="Bootstrap" src="http://getbootstrap.com/assets/brand/bootstrap-solid.svg" width=100 />|  |
|
| <img src="https://raw.githubusercontent.com/voodootikigod/logo.js/master/js.png" alt="JS" width=100 /> | <img alt="preact" src="https://cdn.rawgit.com/developit/b4416d5c92b743dbaec1e68bc4c27cda/raw/3235dc508f7eb834ebf48418aea212a05df13db1/preact-logo-trans.svg" width=200 /> <img alt="Redux" src="https://camo.githubusercontent.com/f28b5bc7822f1b7bb28a96d8d09e7d79169248fc/687474703a2f2f692e696d6775722e636f6d2f4a65567164514d2e706e67" width=200 /> | <img alt="Bootstrap" src="http://getbootstrap.com/assets/brand/bootstrap-solid.svg" width=100 />|  |
|
||||||
| [ES6](http://www.ecma-international.org/publications/standards/Ecma-262.htm) | [React](https://facebook.github.io/react/) + [Redux](http://redux.js.org/) | [Less](http://lesscss.org/) + [Bootstrap](http://getbootstrap.com/) | [CodeMirror](http://codemirror.net/)
|
| [ES2017](http://www.ecma-international.org/publications/standards/Ecma-262.htm) | [preact](https://preact.com/) + [Redux](http://redux.js.org/) | [Bootstrap](http://getbootstrap.com/) | [CodeMirror](http://codemirror.net/)
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Technical Notes
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
### Preact
|
||||||
|
- using [preact](https://github.com/developit/preact) for a smaller setup that
|
||||||
|
is compatible with React.
|
||||||
|
- [importing preact into modules](https://github.com/developit/preact#import-what-you-need)
|
||||||
|
- using [preact-compat](https://www.npmjs.com/package/preact-compat) to avoid
|
||||||
|
migrating all of React code to preact.
|
||||||
|
- [server side rendering in preact](http://thecodebarbarian.com/server-side-rendering-with-preact-and-firebase.html)
|
||||||
|
|
||||||
|
### webpack
|
||||||
|
- use `process.env.MY_FLAG' using
|
||||||
|
[webpack's DefinePlugin to pass in envs](https://webpack.js.org/plugins/define-plugin/#feature-flags).
|
||||||
|
- this is useful when dealing with separating server-only and client-only code.
|
139
package.json
139
package.json
|
@ -1,59 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "rookeries",
|
"name": "rookeries",
|
||||||
"version": "0.6.3",
|
"version": "0.6.4",
|
||||||
"description": "A developer and designer friendly web platform for building gorgeous sites, blogs and portfolios.",
|
"description": "A developer and designer friendly web platform for building gorgeous sites, blogs and portfolios.",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0 <7.0.0"
|
"node": ">=8.0.0 <9.0.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"babel-polyfill": "^6.5.0",
|
|
||||||
"bootstrap": "3.2.0",
|
|
||||||
"codemirror": "^5.17.0",
|
|
||||||
"es6-promise": "^3.1.2",
|
|
||||||
"font-awesome": "4.3.0",
|
|
||||||
"highlight.js": "8.3.0",
|
|
||||||
"isomorphic-fetch": "^2.2.1",
|
|
||||||
"marked": "0.3.3",
|
|
||||||
"node-localstorage": "^1.3.0",
|
|
||||||
"node-uuid": "^1.4.7",
|
|
||||||
"react": "^15.3.0",
|
|
||||||
"react-bootstrap": "^0.30.1",
|
|
||||||
"react-codemirror": "^0.2.6",
|
|
||||||
"react-dom": "^15.3.0",
|
|
||||||
"react-fontawesome": "^1.1.0",
|
|
||||||
"react-redux": "^4.4.5",
|
|
||||||
"react-router": "^2.6.1",
|
|
||||||
"react-router-redux": "^4.0.0",
|
|
||||||
"redux": "^3.3.1",
|
|
||||||
"redux-logger": "^2.6.1",
|
|
||||||
"redux-thunk": "^2.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"babel-cli": "^6.24.0",
|
|
||||||
"babel-core": "^6.13.2",
|
|
||||||
"babel-eslint": "^6.1.2",
|
|
||||||
"babel-plugin-rewire": "^1.0.0-rc-5",
|
|
||||||
"babel-preset-es2015": "^6.9.0",
|
|
||||||
"babel-preset-react": "^6.11.1",
|
|
||||||
"babel-preset-stage-0": "^6.5.0",
|
|
||||||
"babel-register": "^6.11.6",
|
|
||||||
"babel-template": "^6.9.0",
|
|
||||||
"babel-traverse": "^6.13.0",
|
|
||||||
"babel-types": "^6.13.0",
|
|
||||||
"babelify": "^7.3.0",
|
|
||||||
"browserify": "^14.1.0",
|
|
||||||
"chai": "1.9.2",
|
|
||||||
"eslint": "^3.17.1",
|
|
||||||
"eslint-plugin-import": "^1.12.0",
|
|
||||||
"eslint-plugin-react": "^6.0.0",
|
|
||||||
"jsdom": "^9.4.1",
|
|
||||||
"less": "^2.7.2",
|
|
||||||
"mocha": "^3.2.0",
|
|
||||||
"react-addons-test-utils": "^15.3.0",
|
|
||||||
"rewireify": "0.2.1",
|
|
||||||
"sinon": "1.17.6",
|
|
||||||
"sinon-chai": "2.7.0",
|
|
||||||
"watchify": "3.2.2"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -65,18 +15,81 @@
|
||||||
"url": "https://bitbucket.org/dorianpula/rookeries/issues"
|
"url": "https://bitbucket.org/dorianpula/rookeries/issues"
|
||||||
},
|
},
|
||||||
"browser": "./src/index.js",
|
"browser": "./src/index.js",
|
||||||
"babel": {
|
"dependencies": {
|
||||||
"presets": [
|
"babel-polyfill": "^6.5.0",
|
||||||
"es2015",
|
"bootstrap": "4.0.0-beta",
|
||||||
"react",
|
"codemirror": "^5.30.0",
|
||||||
"stage-0"
|
"es6-promise": "^4.1.1",
|
||||||
],
|
"font-awesome": "4.7.0",
|
||||||
"sourceMaps": true
|
"highlight.js": "9.12.0",
|
||||||
|
"isomorphic-fetch": "^2.2.1",
|
||||||
|
"markdown-it": "^8.4.0",
|
||||||
|
"node-localstorage": "^1.3.0",
|
||||||
|
"node-uuid": "^1.4.7",
|
||||||
|
"preact": "^8.2.5",
|
||||||
|
"preact-compat": "^3.17.0",
|
||||||
|
"preact-redux": "^2.0.2",
|
||||||
|
"preact-transition-group": "^1.1.1",
|
||||||
|
"prop-types": "^15.5.10",
|
||||||
|
"react": "^15.6.1",
|
||||||
|
"react-bootstrap": "^0.31.3",
|
||||||
|
"react-codemirror2": "^2.0.0",
|
||||||
|
"react-dom": "^15.6.1",
|
||||||
|
"react-fontawesome": "^1.1.0",
|
||||||
|
"react-redux": "^5.0.6",
|
||||||
|
"react-router": "4.2.0",
|
||||||
|
"react-router-dom": "^4.2.2",
|
||||||
|
"react-transition-group": "1.2.0",
|
||||||
|
"reactstrap": "^4.8.0",
|
||||||
|
"redux": "^3.3.1",
|
||||||
|
"redux-logger": "^3.0.6",
|
||||||
|
"redux-thunk": "^2.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-cli": "^6.24.0",
|
||||||
|
"babel-eslint": "^8.0.0",
|
||||||
|
"babel-loader": "^7.1.2",
|
||||||
|
"babel-minify-webpack-plugin": "^0.2.0",
|
||||||
|
"babel-plugin-module-resolver": "^2.7.1",
|
||||||
|
"babel-plugin-rewire": "^1.1.0",
|
||||||
|
"babel-plugin-syntax-async-functions": "^6.13.0",
|
||||||
|
"babel-preset-es2015": "^6.9.0",
|
||||||
|
"babel-preset-es2017": "^6.24.1",
|
||||||
|
"babel-preset-minify": "^0.2.0",
|
||||||
|
"babel-preset-react": "^6.11.1",
|
||||||
|
"babel-preset-stage-0": "^6.5.0",
|
||||||
|
"babel-register": "^6.11.6",
|
||||||
|
"babel-template": "^6.9.0",
|
||||||
|
"babel-traverse": "^6.13.0",
|
||||||
|
"babel-types": "^6.13.0",
|
||||||
|
"chai": "4.1.2",
|
||||||
|
"css-loader": "^0.28.7",
|
||||||
|
"eslint": "^4.7.2",
|
||||||
|
"eslint-loader": "^1.9.0",
|
||||||
|
"eslint-plugin-import": "^2.7.0",
|
||||||
|
"eslint-plugin-react": "^7.3.0",
|
||||||
|
"extract-text-webpack-plugin": "^3.0.0",
|
||||||
|
"file-loader": "^0.11.2",
|
||||||
|
"html-loader": "^0.5.1",
|
||||||
|
"html-webpack-plugin": "^2.30.1",
|
||||||
|
"jsdom": "^11.2.0",
|
||||||
|
"jsdom-global": "^3.0.2",
|
||||||
|
"mocha": "^3.2.0",
|
||||||
|
"preact-jsx-chai": "^2.2.1",
|
||||||
|
"react-addons-test-utils": "^15.6.0",
|
||||||
|
"sinon": "3.3.0",
|
||||||
|
"style-loader": "^0.18.2",
|
||||||
|
"url-loader": "^0.5.9",
|
||||||
|
"webpack": "^3.6.0",
|
||||||
|
"webpack-dev-server": "^2.8.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npm run build-client && npm run build-css && npm run build-server",
|
"build-client": "webpack",
|
||||||
"build-client": "browserify src/index.js --outfile static/js/rookeries-client.js --transform [ babelify ]",
|
"build-server": "babel src -d dist",
|
||||||
"build-css": "lessc static/css/rookeries-css.less static/css/rookeries-bundle.css",
|
"build": "npm run build-client && npm run build-server",
|
||||||
"build-server": "babel src --out-dir dist --source-maps"
|
"dev-server": "webpack-dev-server",
|
||||||
|
"lint": "eslint src tests/unit",
|
||||||
|
"lint-fix": "eslint --fix src test/unit",
|
||||||
|
"test": "BABEL_DISABLE_CACHE=yes mocha --require tests/js/babelhook --require jsdom-global/register tests/js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ A developer and designer friendly web platform for building gorgeous sites, blog
|
||||||
**Rookeries** is:
|
**Rookeries** is:
|
||||||
|
|
||||||
- Powered by Flask, Python and PostgreSQL on the server side.
|
- Powered by Flask, Python and PostgreSQL on the server side.
|
||||||
- Uses React, Redux, ES6 and LESS to build responsive single page apps.
|
- Uses preact, redux, ES2017 and webpack to build responsive single page apps.
|
||||||
- Licensed under the Affero GNU General Public License (AGPL) version 3.0.
|
- Licensed under the Affero GNU General Public License (AGPL) version 3.0.
|
||||||
|
|
||||||
## Build Status:
|
## Build Status:
|
||||||
|
@ -26,11 +26,10 @@ In the meantime, please refer to the development guide below.
|
||||||
|
|
||||||
*Rookeries* uses the following technologies:
|
*Rookeries* uses the following technologies:
|
||||||
|
|
||||||
- Python 2.7
|
- Python 3.6
|
||||||
- NodeJS 6.9 + ES6
|
- NodeJS 8 + ES2017
|
||||||
- PostgreSQL 9.6
|
- PostgreSQL 9.6
|
||||||
- Docker + docker-compose
|
- Docker + docker-compose
|
||||||
- Make
|
|
||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ db = flask_sqlalchemy.SQLAlchemy()
|
||||||
migrations = flask_alembic.Alembic()
|
migrations = flask_alembic.Alembic()
|
||||||
|
|
||||||
EXTERNAL_DB_URI_PARAM = 'ROOKERIES_SQLALCHEMY_DATABASE_URI'
|
EXTERNAL_DB_URI_PARAM = 'ROOKERIES_SQLALCHEMY_DATABASE_URI'
|
||||||
DEFAULT_DB_CONNECTION = 'postgresql+psycopg2://admin:password@db:5432/rookeries'
|
DEFAULT_DB_CONNECTION = 'postgresql+psycopg2://admin:password@localhost:5432/rookeries'
|
||||||
|
|
||||||
|
|
||||||
def get_db_connection_from_env():
|
def get_db_connection_from_env():
|
||||||
|
|
|
@ -17,8 +17,9 @@ from rookeries.vendors import flask_jwt
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@rookeries_app.route('/api/pages/', methods=['GET'])
|
||||||
@rookeries_app.route('/api/pages/<slug>', methods=['GET'])
|
@rookeries_app.route('/api/pages/<slug>', methods=['GET'])
|
||||||
def serve_page(slug):
|
def serve_page(slug=''):
|
||||||
"""
|
"""
|
||||||
Serving up a page with Markdown content.
|
Serving up a page with Markdown content.
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
"""
|
"""
|
||||||
Views for managing sites.
|
Views for managing sites.
|
||||||
|
|
||||||
:copyright: Copyright 2013-2017, Dorian Pula <dorian.pula@amber-penguin-software.ca>
|
:copyright: Copyright 2013-2017, Dorian Pula
|
||||||
|
<dorian.pula@amber-penguin-software.ca>
|
||||||
:license: AGPL v3+
|
:license: AGPL v3+
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -24,7 +25,8 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def add_self_link(site_json):
|
def add_self_link(site_json):
|
||||||
# TODO: Consider creating a better way to resolve IDs in links to resources, in a generic manner.
|
# TODO: Consider creating a better way to resolve IDs in links to
|
||||||
|
# TODO: resources, in a generic manner.
|
||||||
site_json['urls'] = {
|
site_json['urls'] = {
|
||||||
'self': flask.url_for('rookeries_app.get_site', _external=True),
|
'self': flask.url_for('rookeries_app.get_site', _external=True),
|
||||||
}
|
}
|
||||||
|
@ -40,7 +42,8 @@ def update_site():
|
||||||
flask.abort(http.HTTPStatus.BAD_REQUEST)
|
flask.abort(http.HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
incoming_request = flask.request.get_json()
|
incoming_request = flask.request.get_json()
|
||||||
jsonschema.validate(incoming_request, schema.SITE_CREATION_MODIFICATION_SCHEMA)
|
jsonschema.validate(
|
||||||
|
incoming_request, schema.SITE_CREATION_MODIFICATION_SCHEMA)
|
||||||
|
|
||||||
# Check if user allowed to modify a site.
|
# Check if user allowed to modify a site.
|
||||||
current_user = flask_jwt.current_identity
|
current_user = flask_jwt.current_identity
|
||||||
|
@ -50,7 +53,8 @@ def update_site():
|
||||||
flask.abort(http.HTTPStatus.UNAUTHORIZED)
|
flask.abort(http.HTTPStatus.UNAUTHORIZED)
|
||||||
|
|
||||||
# Modifies a site from the JSON.
|
# Modifies a site from the JSON.
|
||||||
existing_site = models.Site.query.filter_by(name=models.MAIN_SITE_ROOT_BLOCK).first_or_404()
|
existing_site = models.Site.query\
|
||||||
|
.filter_by(name=models.MAIN_SITE_ROOT_BLOCK).first_or_404()
|
||||||
updated_site = models.Site.from_json(incoming_request)
|
updated_site = models.Site.from_json(incoming_request)
|
||||||
existing_site.base_url = updated_site.base_url
|
existing_site.base_url = updated_site.base_url
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -61,6 +65,20 @@ def update_site():
|
||||||
|
|
||||||
@rookeries_app.route('/api/site', methods=['GET'])
|
@rookeries_app.route('/api/site', methods=['GET'])
|
||||||
def get_site():
|
def get_site():
|
||||||
site = models.Site.query.filter_by(name=models.MAIN_SITE_ROOT_BLOCK).first_or_404()
|
# site = models.Site.query.filter_by(name=models.MAIN_SITE_ROOT_BLOCK)\
|
||||||
site_response = add_self_link(site.to_json())
|
# .first_or_404()
|
||||||
|
# site_response = add_self_link(site.to_json())
|
||||||
|
|
||||||
|
# TODO: Use mock site until we figure out setup.
|
||||||
|
site_response = {
|
||||||
|
'name': 'Rookeries',
|
||||||
|
'favicon': '/static/images/mr-penguin-amber-favicon.ico',
|
||||||
|
'config': {
|
||||||
|
'footer': 'Rookeries - © 2013-2016 - Amber Penguin Software',
|
||||||
|
'name': 'Rookeries',
|
||||||
|
'tagline': 'Simple Site Construction',
|
||||||
|
'logo': '/static/images/mr-penguin-amber.svg',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return flask.jsonify(site_response)
|
return flask.jsonify(site_response)
|
||||||
|
|
|
@ -95,7 +95,7 @@ def render_single_page_app(some_path=''):
|
||||||
page_info.update(extract_json_response(page_response))
|
page_info.update(extract_json_response(page_response))
|
||||||
|
|
||||||
# TODO: If no page given, use the default page of a site.
|
# TODO: If no page given, use the default page of a site.
|
||||||
response = invoke.run(f'node dist/index.js /{some_path}')
|
response = invoke.run(f'node dist/server.js /{some_path}')
|
||||||
|
|
||||||
# TODO: Update site and page info with good defaults
|
# TODO: Update site and page info with good defaults
|
||||||
site_info['name'] = 'Rookeries'
|
site_info['name'] = 'Rookeries'
|
||||||
|
|
338
src/actions.js
338
src/actions.js
|
@ -25,237 +25,215 @@ export const SWITCH_THEME_ACTION = "SWITCH_THEME";
|
||||||
|
|
||||||
|
|
||||||
export function initializeApp(applicationStore) {
|
export function initializeApp(applicationStore) {
|
||||||
// TODO: Add ability to load application state / persistent settings from local storage.
|
// TODO: Add ability to load application state / persistent settings from local storage.
|
||||||
applicationStore.dispatch(fetchSiteInfo());
|
applicationStore.dispatch(fetchSiteInfo());
|
||||||
applicationStore.dispatch(initialUser());
|
applicationStore.dispatch(initialUser());
|
||||||
applicationStore.dispatch(fetchSiteMenu());
|
applicationStore.dispatch(fetchSiteMenu());
|
||||||
applicationStore.dispatch(loadPage());
|
applicationStore.dispatch(loadPage());
|
||||||
applicationStore.dispatch(loadTheme());
|
applicationStore.dispatch(loadTheme());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchPage(pageSlug='') {
|
export function fetchPage(pageSlug='') {
|
||||||
return (dispatch) => {
|
return async (dispatch) => {
|
||||||
|
|
||||||
let pageUrl = `/api/pages/${pageSlug}`;
|
const response = await fetch(`/api/pages/${pageSlug}`);
|
||||||
if (pageSlug === '') {
|
// TODO: Add better error handling.
|
||||||
console.info(`Loading landing page.`);
|
|
||||||
// TODO: Retrieve based on the site's default page
|
|
||||||
} else {
|
|
||||||
console.info(`Loading... ${pageSlug}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch(pageUrl)
|
const json = await response.json();
|
||||||
.then((response) => {
|
|
||||||
// TODO: Add better error handling.
|
// TODO: Figure out nicer article/block API.
|
||||||
// if (response.ok) {
|
let articleContent = Reflect.get(json, 'content');
|
||||||
// return response.json();
|
let title = Reflect.get(json, 'title');
|
||||||
// } else {
|
if (articleContent === undefined) {
|
||||||
// console.error(`Error getting document JSON - ${res.status} - ${res.text} - Actual ${err}`);
|
if (pageSlug === 'error') {
|
||||||
// }
|
articleContent = `ERROR - No content found for "${pageSlug}"!`;
|
||||||
return response.json();
|
} else {
|
||||||
})
|
dispatch(fetchPage("error"));
|
||||||
.then((json) => {
|
}
|
||||||
// TODO: Figure out nicer article/block API.
|
|
||||||
let articleContent = Reflect.get(json, 'content');
|
|
||||||
let title = Reflect.get(json, 'title');
|
|
||||||
if (articleContent === undefined) {
|
|
||||||
if (pageSlug === 'error') {
|
|
||||||
articleContent = `ERROR - No content found for "${pageSlug}"!`;
|
|
||||||
} else {
|
|
||||||
dispatch(fetchPage("error"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dispatch(loadPage({id: pageSlug, content: articleContent, title: title}));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return dispatch(loadPage({slug: pageSlug, content: articleContent, title: title}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchSiteMenu() {
|
export function fetchSiteMenu() {
|
||||||
// TODO: Retrieve the name of the site.
|
// TODO: Retrieve the name of the site.
|
||||||
return (dispatch) => {
|
return async (dispatch) => {
|
||||||
return fetch(`/api/site/menu`)
|
|
||||||
.then((response) => {
|
const response = await fetch('/api/site/menu');
|
||||||
// TODO: Add better error handling.
|
// TODO: Add better error handling.
|
||||||
// if (response.ok) {
|
const json = await response.json();
|
||||||
// return response.json();
|
|
||||||
// } else {
|
return dispatch(loadNavigation(json.menu));
|
||||||
// console.error(`Error getting document JSON - ${res.status} - ${res.text} - Actual ${err}`);
|
}
|
||||||
// }
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then((json) => {
|
|
||||||
return dispatch(loadNavigation(json.menu));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchSiteInfo() {
|
export function fetchSiteInfo() {
|
||||||
// TODO: Retrieve the name of the site.
|
// TODO: Retrieve the name of the site.
|
||||||
return (dispatch) => {
|
return async (dispatch) => {
|
||||||
return fetch("/api/site")
|
|
||||||
.then((response) => {
|
|
||||||
|
|
||||||
// TODO: Add better error handling.
|
const response = await fetch("/api/site");
|
||||||
// if (response.ok) {
|
|
||||||
// return response.json();
|
|
||||||
// } else {
|
|
||||||
// console.error(`Error getting document JSON - ${res.status} - ${res.text} - Actual ${err}`);
|
|
||||||
// }
|
|
||||||
|
|
||||||
return response.json();
|
// TODO: Add better error handling.
|
||||||
})
|
const json = response.json();
|
||||||
.then((json) => {
|
|
||||||
// TODO: Avoid this extra conversion
|
// TODO: Avoid this extra conversion
|
||||||
let appSiteInfo = {
|
const appSiteInfo = {
|
||||||
footer: json.config.footer,
|
footer: json.config.footer,
|
||||||
logo: json.config.logo,
|
logo: json.config.logo,
|
||||||
name: json.config.name,
|
name: json.config.name,
|
||||||
tagline: json.config.tagline
|
tagline: json.config.tagline
|
||||||
};
|
};
|
||||||
return dispatch(updateSiteInfo(appSiteInfo));
|
|
||||||
});
|
return dispatch(updateSiteInfo(appSiteInfo));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateSiteInfo(siteInfo) {
|
export function updateSiteInfo(siteInfo) {
|
||||||
return {
|
return {
|
||||||
type: UPDATE_SITE_INFO_ACTION,
|
type: UPDATE_SITE_INFO_ACTION,
|
||||||
site: siteInfo
|
site: siteInfo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement this better.
|
|
||||||
export function savePage(content) {
|
export function savePage(content) {
|
||||||
let pageSlug = content.slug;
|
return async (dispatch) => {
|
||||||
|
|
||||||
return (dispatch) => {
|
const response = await fetch(`/api/pages/${content.id}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `JWT ${UserInfo.getUserInfo('auth_token')}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(content)
|
||||||
|
});
|
||||||
|
|
||||||
let pageUrl = `/api/pages/${pageSlug}`;
|
const json = await response.json();
|
||||||
return fetch(pageUrl, {
|
|
||||||
method: "POST",
|
// TODO: Figure out nicer article/block API.
|
||||||
headers: {
|
let articleContent = Reflect.get(json, 'content');
|
||||||
'Accept': 'application/json',
|
let title = Reflect.get(json, 'title');
|
||||||
'Content-Type': 'application/json'
|
let articleId = Reflect.get(json, 'id');
|
||||||
},
|
if (articleContent === undefined) {
|
||||||
body: JSON.stringify(content)
|
if (articleId === 'error') {
|
||||||
}).then((response) => {
|
articleContent = `ERROR - No content found for "${articleId}"!`;
|
||||||
return response.json();
|
} else {
|
||||||
}).then((json) => {
|
dispatch(fetchPage("error"));
|
||||||
// TODO: Figure out nicer article/block API.
|
}
|
||||||
let articleContent = Reflect.get(json, 'content');
|
|
||||||
let title = Reflect.get(json, 'title');
|
|
||||||
let articleId = Reflect.get(json, 'id');
|
|
||||||
if (articleContent === undefined) {
|
|
||||||
if (articleId === 'error') {
|
|
||||||
articleContent = `ERROR - No content found for "${articleId}"!`;
|
|
||||||
} else {
|
|
||||||
dispatch(fetchPage("error"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dispatch(loadPage({id: articleId, content: articleContent, title: title}));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return dispatch(loadPage({id: articleId, content: articleContent, title: title}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadPage(article={content: 'Loading content... Please wait.', title: 'Loading...'}) {
|
export function loadPage(article={content: 'Loading content... Please wait.', title: 'Loading...'}) {
|
||||||
// TODO: Extract editor setup and session info...
|
// TODO: Extract editor setup and session info...
|
||||||
return {
|
return {
|
||||||
type: LOAD_PAGE_ACTION,
|
type: LOAD_PAGE_ACTION,
|
||||||
content: {...article}
|
content: {...article}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadNavigation(menu=[]) {
|
export function loadNavigation(menu=[]) {
|
||||||
return {
|
return {
|
||||||
type: LOAD_MENU_ACTION,
|
type: LOAD_MENU_ACTION,
|
||||||
menu: menu
|
menu: menu
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function switchThemes() {
|
export function switchThemes() {
|
||||||
let switchedThemes = ThemeManager.switchTheme();
|
let switchedThemes = ThemeManager.switchTheme();
|
||||||
return {
|
return {
|
||||||
type: SWITCH_THEME_ACTION,
|
type: SWITCH_THEME_ACTION,
|
||||||
theme: switchedThemes.theme,
|
theme: switchedThemes.theme,
|
||||||
alternativeTheme: switchedThemes.alternativeTheme,
|
alternativeTheme: switchedThemes.alternativeTheme,
|
||||||
editorTheme: switchedThemes.editorTheme
|
editorTheme: switchedThemes.editorTheme
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadTheme() {
|
export function loadTheme() {
|
||||||
return {
|
return {
|
||||||
type: LOAD_THEME_ACTION,
|
type: LOAD_THEME_ACTION,
|
||||||
theme: ThemeManager.getCurrentTheme(),
|
theme: ThemeManager.getCurrentTheme(),
|
||||||
alternativeTheme: ThemeManager.getNextTheme(),
|
alternativeTheme: ThemeManager.getNextTheme(),
|
||||||
editorTheme: ThemeManager.getEditorTheme()
|
editorTheme: ThemeManager.getEditorTheme()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function initialUser() {
|
export function initialUser() {
|
||||||
return {
|
return {
|
||||||
type: LOAD_INITIAL_USER_ACTION,
|
type: LOAD_INITIAL_USER_ACTION,
|
||||||
user: {
|
user: {
|
||||||
token: UserInfo.getUserInfo("auth_token") || "",
|
token: UserInfo.getUserInfo("auth_token") || "",
|
||||||
fullName: UserInfo.getUserInfo("full_name") || "Stranger",
|
fullName: UserInfo.getUserInfo("full_name") || "Stranger",
|
||||||
accountState: ""
|
accountState: ""
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateUser(token="", user=DEFAULT_USER_LOGIN, accountState="") {
|
export function updateUser(token="", user=DEFAULT_USER_LOGIN, accountState="") {
|
||||||
if (token !== "") {
|
if (token !== "") {
|
||||||
UserInfo.setUserInfo("auth_token", token);
|
UserInfo.setUserInfo("auth_token", token);
|
||||||
}
|
}
|
||||||
UserInfo.setUserInfo("full_name", user.fullName);
|
UserInfo.setUserInfo("full_name", user.profile.fullName);
|
||||||
return {
|
return {
|
||||||
type: UPDATE_USER_ACTION,
|
type: UPDATE_USER_ACTION,
|
||||||
user: {
|
user: {
|
||||||
...user,
|
...user,
|
||||||
token: token,
|
token: token,
|
||||||
accountState: accountState
|
accountState: accountState
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logoutUser(accountState="") {
|
export function logoutUser(accountState="") {
|
||||||
UserInfo.removeUserInfo("auth_token");
|
UserInfo.removeUserInfo("auth_token");
|
||||||
UserInfo.removeUserInfo("full_name");
|
UserInfo.removeUserInfo("full_name");
|
||||||
return {
|
return {
|
||||||
type: LOGOUT_USER_ACTION,
|
type: LOGOUT_USER_ACTION,
|
||||||
user: {
|
user: {
|
||||||
...DEFAULT_USER_LOGIN,
|
...DEFAULT_USER_LOGIN,
|
||||||
token: "",
|
token: "",
|
||||||
accountState: accountState
|
accountState: accountState
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function loginUser(username, password) {
|
export function loginUser(username, password) {
|
||||||
return (dispatch) => {
|
return async (dispatch) => {
|
||||||
return fetch("/auth", {
|
|
||||||
method: "POST",
|
const response = await fetch("/auth", {
|
||||||
headers: {
|
method: "POST",
|
||||||
'Accept': 'application/json',
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Accept': 'application/json',
|
||||||
},
|
'Content-Type': 'application/json'
|
||||||
body: JSON.stringify({username: username, password: password})
|
},
|
||||||
}).then((response) => {
|
body: JSON.stringify({username: username, password: password})
|
||||||
// TODO: Turn this into a better action:
|
});
|
||||||
if (response.status !== 200 && response.status !== 401) {
|
|
||||||
// TODO: Deal with resolving the promise first.
|
if (response.status !== 200 && response.status !== 401) {
|
||||||
return dispatch(logoutUser(`Unable to login "${username}".`));
|
return dispatch(logoutUser(`Unable to login "${username}".`));
|
||||||
}
|
|
||||||
// TODO: Refer to: https://github.com/github/fetch
|
|
||||||
return response.json();
|
|
||||||
}).then((json) => {
|
|
||||||
let authToken = Reflect.get(json, "token");
|
|
||||||
if (authToken !== undefined) {
|
|
||||||
let user = Reflect.get(json, "user");
|
|
||||||
return dispatch(updateUser(authToken, user));
|
|
||||||
}
|
|
||||||
return dispatch(logoutUser(`${json.description} for "${username}".`));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
let authToken = Reflect.get(json, "access_token");
|
||||||
|
if (authToken !== undefined) {
|
||||||
|
|
||||||
|
const username = Reflect.get(json, "username");
|
||||||
|
const userResponse = await fetch(`/api/users/${username}`,{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
'Authorization': `JWT ${authToken}`,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const user = await userResponse.json();
|
||||||
|
return dispatch(updateUser(authToken, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispatch(logoutUser(`${json.description} for "${username}".`));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,12 @@ Rookeries client app
|
||||||
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import {h, Component} from "preact"; /** @jsx h*/
|
||||||
|
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {Switch, Route, withRouter} from "react-router-dom";
|
||||||
|
import {Container, Row, Col} from "reactstrap";
|
||||||
|
|
||||||
import {ConnectedUserLogin} from "../views/user_login_modal";
|
import {ConnectedUserLogin} from "../views/user_login_modal";
|
||||||
import {ConnectedThemeSwitchView} from "../views/theme_switcher_button";
|
import {ConnectedThemeSwitchView} from "../views/theme_switcher_button";
|
||||||
|
@ -15,61 +19,60 @@ import {ConnectedThemeSwitchView} from "../views/theme_switcher_button";
|
||||||
import {SiteHeader} from "../views/site_header";
|
import {SiteHeader} from "../views/site_header";
|
||||||
import {SiteFooter} from "../views/site_footer";
|
import {SiteFooter} from "../views/site_footer";
|
||||||
import {ConnectedNavigationMenu} from "../views/navigation_menu";
|
import {ConnectedNavigationMenu} from "../views/navigation_menu";
|
||||||
|
import {ConnectedArticle} from "../views/journal_markdown_viewer";
|
||||||
|
|
||||||
|
|
||||||
// TODO: Add in React Bootstrap rows + columns.
|
// TODO: Add in React Bootstrap rows + columns.
|
||||||
class App extends React.Component {
|
class App extends Component {
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
return {
|
return {
|
||||||
footer: React.PropTypes.string,
|
footer: PropTypes.string,
|
||||||
siteName: React.PropTypes.string,
|
siteName: PropTypes.string,
|
||||||
logo: React.PropTypes.string,
|
logo: PropTypes.string,
|
||||||
tagline: React.PropTypes.string
|
tagline: PropTypes.string
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Container>
|
||||||
{/*TODO Extract as header container.*/}
|
{/*TODO Extract as header container.*/}
|
||||||
<div className="row">
|
<Row className="justify-content-around">
|
||||||
<div className="col-lg-offset-1 col-lg-3">
|
<Col lg="4">
|
||||||
<ConnectedUserLogin />
|
<ConnectedUserLogin />
|
||||||
</div>
|
</Col>
|
||||||
<div className="col-lg-offset-5 col-lg-2">
|
<Col lg="4">
|
||||||
<ConnectedThemeSwitchView />
|
<ConnectedThemeSwitchView />
|
||||||
</div>
|
</Col>
|
||||||
</div>
|
</Row>
|
||||||
|
|
||||||
{/*TODO Extract as site header container.*/}
|
{/*TODO Extract as site header container.*/}
|
||||||
<div className="row">
|
<SiteHeader siteName={ this.props.siteName } logo={ this.props.logo } tagline={ this.props.tagline } />
|
||||||
<SiteHeader siteName={ this.props.siteName } logo={ this.props.logo } tagline={ this.props.tagline } />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*TODO Extract as Main body and navigation sidebar containers.*/}
|
{/*TODO Extract as Main body and navigation sidebar containers.*/}
|
||||||
<div className="row">
|
<Row className="justify-content-center">
|
||||||
<div><ConnectedNavigationMenu /></div>
|
<ConnectedNavigationMenu />
|
||||||
<div className="col-lg-8 ice-floe">
|
<Col lg="8" className="ice-floe">
|
||||||
<div>
|
<Switch>
|
||||||
{ this.props.children }
|
<Route exact path="/" component={ ConnectedArticle } />
|
||||||
</div>
|
<Route path="/:pageId" component={ ConnectedArticle } />
|
||||||
</div>
|
</Switch>
|
||||||
</div>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
{/*TODO Extract as site footer container.*/}
|
{/*TODO Extract as site footer container.*/}
|
||||||
<div className="row">
|
<SiteFooter footer={ this.props.footer } />
|
||||||
<SiteFooter footer={ this.props.footer } />
|
</Container>
|
||||||
</div>
|
);
|
||||||
</div>
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WiredApp = connect(
|
export const WiredApp = withRouter(connect(
|
||||||
(state) => {
|
(state) => {
|
||||||
return {
|
return {
|
||||||
'footer': state.site.footer,
|
'footer': state.site.footer,
|
||||||
'siteName': state.site.name,
|
'siteName': state.site.name,
|
||||||
'logo': state.site.logo,
|
'logo': state.site.logo,
|
||||||
'tagline': state.site.tagline
|
'tagline': state.site.tagline
|
||||||
};
|
};
|
||||||
})(App);
|
})(App));
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
Rookeries - Entrypoint for browser app
|
||||||
|
|
||||||
|
@copyright (c) Copyright 2013-2017 Dorian Pula
|
||||||
|
@license AGPL v3
|
||||||
|
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import {h, render} from "preact"; /** @jsx h*/
|
||||||
|
|
||||||
|
import '../static/css/rookeries-css.css';
|
||||||
|
import {BrowserRouter} from "react-router-dom";
|
||||||
|
import {Provider} from "react-redux";
|
||||||
|
|
||||||
|
import {appStore} from "./stores";
|
||||||
|
import {initializeApp, fetchPage} from "./actions";
|
||||||
|
import {WiredApp} from "./containers/app";
|
||||||
|
|
||||||
|
|
||||||
|
initializeApp(appStore);
|
||||||
|
|
||||||
|
const location = window.location.pathname.substring(1);
|
||||||
|
appStore.dispatch(fetchPage(location));
|
||||||
|
|
||||||
|
const RookeriesWiredApp = (
|
||||||
|
<Provider store={ appStore }>
|
||||||
|
<BrowserRouter>
|
||||||
|
<WiredApp />
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
render(RookeriesWiredApp, document.getElementById("ui-target"));
|
19
src/index.js
19
src/index.js
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
Rookeries client app - main entry point for isomorphic React app.
|
|
||||||
|
|
||||||
@copyright (c) Copyright 2013-2017 Dorian Pula
|
|
||||||
@license AGPL v3
|
|
||||||
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import ClientRouterSetup from "./router/client";
|
|
||||||
import {renderReactComponentsToString} from "./router/server";
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
ClientRouterSetup();
|
|
||||||
} else {
|
|
||||||
// Initialized the app for server-side rendering instead.
|
|
||||||
let bodyResponse = renderReactComponentsToString();
|
|
||||||
console.log(bodyResponse.bodyContent);
|
|
||||||
}
|
|
118
src/reducers.js
118
src/reducers.js
|
@ -7,100 +7,98 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {combineReducers} from 'redux';
|
import {combineReducers} from 'redux';
|
||||||
import {routerReducer} from 'react-router-redux';
|
|
||||||
|
|
||||||
import * as actions from './actions';
|
import * as actions from './actions';
|
||||||
// TODO Improve setup of reducers.
|
// TODO Improve setup of reducers.
|
||||||
|
|
||||||
|
|
||||||
function pageStateReducer(state = "Loading...", action) {
|
function pageStateReducer(state = "Loading...", action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case actions.LOAD_PAGE_ACTION:
|
case actions.LOAD_PAGE_ACTION:
|
||||||
return action.content;
|
return action.content;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function menuStateReducer(state = [], action) {
|
function menuStateReducer(state = [], action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case actions.LOAD_MENU_ACTION:
|
case actions.LOAD_MENU_ACTION:
|
||||||
return action.menu;
|
return action.menu;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const DEFAULT_SITE_INFO = {
|
const DEFAULT_SITE_INFO = {
|
||||||
name: "Rookeries",
|
name: "Rookeries",
|
||||||
logo: "/static/images/mr-penguin-amber.svg",
|
logo: "/static/images/mr-penguin-amber.svg",
|
||||||
tagline: "Simple Site Scaffolding",
|
tagline: "Simple Site Scaffolding",
|
||||||
footer: "Rookeries - © 2013-2016 - Amber Penguin Software"
|
footer: "Rookeries - © 2013-2016 - Amber Penguin Software"
|
||||||
};
|
};
|
||||||
|
|
||||||
function siteInfoStateReducer(state = {...DEFAULT_SITE_INFO}, action) {
|
function siteInfoStateReducer(state = {...DEFAULT_SITE_INFO}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case actions.UPDATE_SITE_INFO_ACTION:
|
case actions.UPDATE_SITE_INFO_ACTION:
|
||||||
return action.site;
|
return action.site;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_USER_LOGIN = {
|
export const DEFAULT_USER_LOGIN = {
|
||||||
fullName: "Stranger",
|
fullName: "Stranger",
|
||||||
token: ""
|
token: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
function userLoginStateReducer(state = {...DEFAULT_USER_LOGIN}, action) {
|
function userLoginStateReducer(state = {...DEFAULT_USER_LOGIN}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case actions.LOAD_INITIAL_USER_ACTION:
|
case actions.LOAD_INITIAL_USER_ACTION:
|
||||||
return action.user;
|
return action.user;
|
||||||
case actions.UPDATE_USER_ACTION:
|
case actions.UPDATE_USER_ACTION:
|
||||||
return action.user;
|
return action.user;
|
||||||
case actions.LOGOUT_USER_ACTION:
|
case actions.LOGOUT_USER_ACTION:
|
||||||
return action.user;
|
return action.user;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function themeStateReducer(state = {}, action) {
|
function themeStateReducer(state = {}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case actions.SWITCH_THEME_ACTION:
|
case actions.SWITCH_THEME_ACTION:
|
||||||
return {theme: action.theme, alternativeTheme: action.alternativeTheme};
|
return {theme: action.theme, alternativeTheme: action.alternativeTheme};
|
||||||
case actions.LOAD_THEME_ACTION:
|
case actions.LOAD_THEME_ACTION:
|
||||||
return {theme: action.theme, alternativeTheme: action.alternativeTheme};
|
return {theme: action.theme, alternativeTheme: action.alternativeTheme};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_EDITOR_STATE = {
|
const DEFAULT_EDITOR_STATE = {
|
||||||
state: 0,
|
state: 0,
|
||||||
theme: "default"
|
theme: "default"
|
||||||
};
|
};
|
||||||
|
|
||||||
function editorStateReducer(state = {...DEFAULT_EDITOR_STATE}, action) {
|
function editorStateReducer(state = {...DEFAULT_EDITOR_STATE}, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case actions.SWITCH_THEME_ACTION:
|
case actions.SWITCH_THEME_ACTION:
|
||||||
return {...state, theme: action.editorTheme};
|
return {...state, theme: action.editorTheme};
|
||||||
case actions.LOAD_THEME_ACTION:
|
case actions.LOAD_THEME_ACTION:
|
||||||
return {...state, theme: action.editorTheme};
|
return {...state, theme: action.editorTheme};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const reducer = combineReducers({
|
const reducer = combineReducers({
|
||||||
page: pageStateReducer,
|
page: pageStateReducer,
|
||||||
editor: editorStateReducer,
|
editor: editorStateReducer,
|
||||||
menu: menuStateReducer,
|
menu: menuStateReducer,
|
||||||
site: siteInfoStateReducer,
|
site: siteInfoStateReducer,
|
||||||
user: userLoginStateReducer,
|
user: userLoginStateReducer,
|
||||||
theme: themeStateReducer,
|
theme: themeStateReducer,
|
||||||
routing: routerReducer
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export {reducer};
|
export {reducer};
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
Rookeries - Router and initialization for client-side
|
|
||||||
|
|
||||||
@copyright (c) Copyright 2013-2017 Dorian Pula
|
|
||||||
@license AGPL v3
|
|
||||||
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import ReactDom from "react-dom";
|
|
||||||
|
|
||||||
import {Router, browserHistory} from "react-router";
|
|
||||||
import {syncHistoryWithStore} from "react-router-redux";
|
|
||||||
import {Provider} from "react-redux";
|
|
||||||
|
|
||||||
import {Routes} from "./routes";
|
|
||||||
import {appStore} from "../stores";
|
|
||||||
import {initializeApp, fetchPage} from "../actions";
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
initializeApp(appStore);
|
|
||||||
const history = syncHistoryWithStore(browserHistory, appStore);
|
|
||||||
|
|
||||||
history.listen((location) => {
|
|
||||||
appStore.dispatch(fetchPage(location.pathname.substring(1)))
|
|
||||||
});
|
|
||||||
|
|
||||||
const RookeriesWiredApp = (
|
|
||||||
<Provider store={ appStore }>
|
|
||||||
<Router history={ history } routes={ Routes }/>
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
ReactDom.render(RookeriesWiredApp, document.getElementById("ui-target"));
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
Rookeries client app - Route setup
|
|
||||||
|
|
||||||
@copyright (c) Copyright 2013-2017 Dorian Pula
|
|
||||||
@license AGPL v3
|
|
||||||
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import {Route, IndexRoute} from 'react-router';
|
|
||||||
|
|
||||||
import {ConnectedArticle} from '../views/journal_markdown_viewer';
|
|
||||||
import {WiredApp} from '../containers/app';
|
|
||||||
|
|
||||||
const Routes = (
|
|
||||||
<Route path="/" component={ WiredApp }>
|
|
||||||
<IndexRoute component={ ConnectedArticle }/>
|
|
||||||
<Route path="/:pageId" component={ ConnectedArticle }/>
|
|
||||||
</Route>
|
|
||||||
);
|
|
||||||
|
|
||||||
export { Routes };
|
|
|
@ -1,39 +0,0 @@
|
||||||
/*
|
|
||||||
Rookeries - Router and initialization for server-side rendering
|
|
||||||
|
|
||||||
@copyright (c) Copyright 2013-2017 Dorian Pula
|
|
||||||
@license AGPL v3
|
|
||||||
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
|
||||||
*/
|
|
||||||
|
|
||||||
import process from "process";
|
|
||||||
import React from "react";
|
|
||||||
import ReactDOMServerSide from "react-dom/server";
|
|
||||||
import {match, RouterContext, createMemoryHistory} from "react-router";
|
|
||||||
import {Provider} from "react-redux";
|
|
||||||
import {syncHistoryWithStore} from "react-router-redux";
|
|
||||||
|
|
||||||
import {serverSideStore} from "../stores";
|
|
||||||
import {Routes} from "./routes";
|
|
||||||
|
|
||||||
|
|
||||||
export function renderReactComponentsToString() {
|
|
||||||
const url = process.argv[2];
|
|
||||||
const memoryHistory = createMemoryHistory(url);
|
|
||||||
const history = syncHistoryWithStore(memoryHistory, serverSideStore);
|
|
||||||
|
|
||||||
const renderedApp = {
|
|
||||||
bodyContent: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
match({history, routes: Routes, location: url}, (err, redirect, props) => {
|
|
||||||
renderedApp.bodyContent = ReactDOMServerSide.renderToString(
|
|
||||||
<Provider store={ serverSideStore }>
|
|
||||||
<RouterContext { ...props } />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return renderedApp;
|
|
||||||
}
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
Rookeries - Server-side renderer
|
||||||
|
|
||||||
|
@copyright (c) Copyright 2013-2017 Dorian Pula
|
||||||
|
@license AGPL v3
|
||||||
|
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {h} from "preact";
|
||||||
|
import {render} from "preact-render-to-string";
|
||||||
|
/** @jsx h */
|
||||||
|
|
||||||
|
import process from "process";
|
||||||
|
|
||||||
|
import {StaticRouter} from "react-router-dom";
|
||||||
|
import {Provider} from "preact-redux";
|
||||||
|
|
||||||
|
import {WiredApp} from "./containers/app";
|
||||||
|
import {serverSideStore} from "./stores";
|
||||||
|
|
||||||
|
export function renderReactComponentsToString() {
|
||||||
|
const url = process.argv[2];
|
||||||
|
|
||||||
|
const context = {};
|
||||||
|
return {
|
||||||
|
bodyContent: render(h(
|
||||||
|
<Provider store={ serverSideStore }>
|
||||||
|
<StaticRouter location={ url } context={ context }>
|
||||||
|
<WiredApp />
|
||||||
|
</StaticRouter>
|
||||||
|
</Provider>
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let bodyResponse = renderReactComponentsToString();
|
||||||
|
console.log(bodyResponse.bodyContent);
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {createStore, applyMiddleware, compose} from "redux";
|
import {createStore, applyMiddleware, compose} from "redux";
|
||||||
import createLogger from "redux-logger";
|
import {createLogger} from "redux-logger";
|
||||||
import thunkMiddleware from "redux-thunk";
|
import thunkMiddleware from "redux-thunk";
|
||||||
|
|
||||||
import {reducer} from './reducers';
|
import {reducer} from './reducers';
|
||||||
|
@ -19,22 +19,22 @@ const loggingMiddleware = createLogger();
|
||||||
// TODO: See https://github.com/chentsulin/electron-react-boilerplate/tree/master/app/store
|
// TODO: See https://github.com/chentsulin/electron-react-boilerplate/tree/master/app/store
|
||||||
|
|
||||||
const appStore = createStore(
|
const appStore = createStore(
|
||||||
reducer,
|
reducer,
|
||||||
compose(
|
compose(
|
||||||
applyMiddleware(loggingMiddleware, thunkMiddleware),
|
applyMiddleware(loggingMiddleware, thunkMiddleware),
|
||||||
// (typeof(window) !== 'undefined' && window.devToolsExtension) ? window.devToolsExtension() : (f) => {return f;}
|
// (typeof(window) !== 'undefined' && window.devToolsExtension) ? window.devToolsExtension() : (f) => {return f;}
|
||||||
typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : (f) => {return f;}
|
typeof window === 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : (f) => {return f;}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Create a factory method for creating the app store.
|
// TODO: Create a factory method for creating the app store.
|
||||||
|
|
||||||
const serverSideStore = createStore(
|
const serverSideStore = createStore(
|
||||||
reducer,
|
reducer,
|
||||||
compose(
|
compose(
|
||||||
applyMiddleware(thunkMiddleware),
|
applyMiddleware(thunkMiddleware),
|
||||||
(f) => {return f;}
|
(f) => {return f;}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
export { appStore, serverSideStore };
|
export { appStore, serverSideStore };
|
||||||
|
|
|
@ -10,46 +10,46 @@ import {UserInfo} from "./user_info";
|
||||||
|
|
||||||
|
|
||||||
class ThemeManager {
|
class ThemeManager {
|
||||||
static getRegisteredThemes() {
|
static getRegisteredThemes() {
|
||||||
return ["daytime", "evening"];
|
return ["daytime", "evening"];
|
||||||
|
}
|
||||||
|
|
||||||
|
static switchTheme() {
|
||||||
|
let currentTheme = ThemeManager.getCurrentTheme();
|
||||||
|
let nextTheme = ThemeManager.getNextTheme();
|
||||||
|
|
||||||
|
let registeredThemes = ThemeManager.getRegisteredThemes();
|
||||||
|
if (registeredThemes.indexOf(nextTheme) === -1) {
|
||||||
|
console.error(`Selected theme '${nextTheme}' is not one of the known themes: '${registeredThemes}'`);
|
||||||
|
nextTheme = registeredThemes[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
static switchTheme() {
|
if (currentTheme !== nextTheme) {
|
||||||
let currentTheme = ThemeManager.getCurrentTheme();
|
console.log(`Adding '${nextTheme}' and removing '${currentTheme}'`);
|
||||||
let nextTheme = ThemeManager.getNextTheme();
|
document.body.classList.remove(currentTheme);
|
||||||
|
document.body.classList.add(nextTheme);
|
||||||
let registeredThemes = ThemeManager.getRegisteredThemes();
|
|
||||||
if (registeredThemes.indexOf(nextTheme) === -1) {
|
|
||||||
console.error(`Selected theme '${nextTheme}' is not one of the known themes: '${registeredThemes}'`);
|
|
||||||
nextTheme = registeredThemes[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTheme !== nextTheme) {
|
|
||||||
console.log(`Adding '${nextTheme}' and removing '${currentTheme}'`);
|
|
||||||
document.body.classList.remove(currentTheme);
|
|
||||||
document.body.classList.add(nextTheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
UserInfo.setUserInfo("theme", nextTheme);
|
|
||||||
return {
|
|
||||||
theme: nextTheme,
|
|
||||||
alternativeTheme: ThemeManager.getNextTheme(),
|
|
||||||
editorTheme: ThemeManager.getEditorTheme()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getCurrentTheme() {
|
UserInfo.setUserInfo("theme", nextTheme);
|
||||||
// TODO: Make function more friendly to server-side rendering via use of redux stores instead.
|
return {
|
||||||
return UserInfo.getUserInfo("theme") || (typeof document !== "undefined" && document.body.classList[0]) || "evening";
|
theme: nextTheme,
|
||||||
}
|
alternativeTheme: ThemeManager.getNextTheme(),
|
||||||
|
editorTheme: ThemeManager.getEditorTheme()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
static getEditorTheme() {
|
static getCurrentTheme() {
|
||||||
return ThemeManager.getCurrentTheme() === "daytime" ? "default" : "monokai";
|
// TODO: Make function more friendly to server-side rendering via use of redux stores instead.
|
||||||
}
|
return UserInfo.getUserInfo("theme") || (typeof document !== "undefined" && document.body.classList[0]) || "evening";
|
||||||
|
}
|
||||||
|
|
||||||
static getNextTheme() {
|
static getEditorTheme() {
|
||||||
return ThemeManager.getCurrentTheme() === "daytime" ? "evening" : "daytime";
|
return ThemeManager.getCurrentTheme() === "daytime" ? "default" : "monokai";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getNextTheme() {
|
||||||
|
return ThemeManager.getCurrentTheme() === "daytime" ? "evening" : "daytime";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {ThemeManager};
|
export {ThemeManager};
|
||||||
|
|
|
@ -7,51 +7,53 @@ User preferences via local storage
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
if (typeof localStorage === "undefined" || localStorage === null) {
|
if (!process.env.BROWSER_ENV) {
|
||||||
|
if (typeof localStorage === "undefined" || localStorage === null) {
|
||||||
const LocalStorage = require("node-localstorage").LocalStorage;
|
const LocalStorage = require("node-localstorage").LocalStorage;
|
||||||
// TODO: Change out mechanism to use other persistance setup.
|
// TODO: Change out mechanism to use other persistance setup.
|
||||||
const FileSystem = require("fs");
|
const FileSystem = require("fs");
|
||||||
const UUID = require("node-uuid");
|
const UUID = require("node-uuid");
|
||||||
let tempDirPath = "/tmp/rookeries";
|
let tempDirPath = "/tmp/rookeries";
|
||||||
try {
|
try {
|
||||||
FileSystem.statSync(tempDirPath).isDirectory();
|
FileSystem.statSync(tempDirPath).isDirectory();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: Figure out how to suppress creation of user info or log messages during server side rendering.
|
// TODO: Figure out how to suppress creation of user info or log messages during server side rendering.
|
||||||
// console.log(`Creating temporary directory: ${tempDirPath}`);
|
// console.log(`Creating temporary directory: ${tempDirPath}`);
|
||||||
FileSystem.mkdirSync(tempDirPath);
|
FileSystem.mkdirSync(tempDirPath);
|
||||||
}
|
}
|
||||||
global.localStorage = new LocalStorage(`${tempDirPath}/scratch-${UUID.v4()}`);
|
global.localStorage = new LocalStorage(`${tempDirPath}/scratch-${UUID.v4()}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class UserInfo {
|
export class UserInfo {
|
||||||
static getInitialInfo() {
|
static getInitialInfo() {
|
||||||
return {
|
return {
|
||||||
theme: "evening",
|
theme: "evening",
|
||||||
editor_theme: "monokai",
|
editor_theme: "monokai",
|
||||||
full_name: "Stranger",
|
full_name: "Stranger",
|
||||||
auth_token: ""
|
auth_token: ""
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getUserInfo(info) {
|
||||||
|
// console.log(`Getting user info '${info}'`);
|
||||||
|
let storageKey = `user_${info}`;
|
||||||
|
let infoItem = localStorage.getItem(storageKey);
|
||||||
|
if (infoItem === undefined) {
|
||||||
|
infoItem = UserInfo.getInitialInfo()[info];
|
||||||
}
|
}
|
||||||
|
|
||||||
static getUserInfo(info) {
|
return infoItem;
|
||||||
// console.log(`Getting user info '${info}'`);
|
}
|
||||||
let storageKey = `user_${info}`;
|
|
||||||
let infoItem = localStorage.getItem(storageKey);
|
|
||||||
if (infoItem === undefined) {
|
|
||||||
infoItem = UserInfo.getInitialInfo()[info];
|
|
||||||
}
|
|
||||||
|
|
||||||
return infoItem;
|
static setUserInfo(info, value) {
|
||||||
}
|
// console.log(`Setting user info '${info}' to '${value}'`);
|
||||||
|
localStorage.setItem(`user_${info}`, value);
|
||||||
|
}
|
||||||
|
|
||||||
static setUserInfo(info, value) {
|
static removeUserInfo(info) {
|
||||||
// console.log(`Setting user info '${info}' to '${value}'`);
|
// console.log(`Clearing user info '${info}'`);
|
||||||
localStorage.setItem(`user_${info}`, value);
|
localStorage.removeItem(`user_${info}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static removeUserInfo(info) {
|
|
||||||
// console.log(`Clearing user info '${info}'`);
|
|
||||||
localStorage.removeItem(`user_${info}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,5 +8,5 @@
|
||||||
|
|
||||||
|
|
||||||
export function formatJson(json) {
|
export function formatJson(json) {
|
||||||
return JSON.stringify(json, null, 2);
|
return JSON.stringify(json, null, 2);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,346 @@
|
||||||
|
// FROM https://raw.githubusercontent.com/reactstrap/reactstrap/master/src/Modal.js
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import {h, Component} from "preact"; /** @jsx h*/
|
||||||
|
import ReactDOM from "preact-compat";
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import TransitionGroup from 'preact-transition-group';
|
||||||
|
import Fade from 'reactstrap/lib/Fade';
|
||||||
|
import {
|
||||||
|
getOriginalBodyPadding,
|
||||||
|
conditionallyUpdateScrollbar,
|
||||||
|
setScrollbarWidth,
|
||||||
|
mapToCssModules,
|
||||||
|
omit
|
||||||
|
} from 'reactstrap/lib/utils';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
isOpen: PropTypes.bool,
|
||||||
|
autoFocus: PropTypes.bool,
|
||||||
|
size: PropTypes.string,
|
||||||
|
toggle: PropTypes.func,
|
||||||
|
keyboard: PropTypes.bool,
|
||||||
|
role: PropTypes.string,
|
||||||
|
labelledBy: PropTypes.string,
|
||||||
|
backdrop: PropTypes.oneOfType([
|
||||||
|
PropTypes.bool,
|
||||||
|
PropTypes.oneOf(['static'])
|
||||||
|
]),
|
||||||
|
onEnter: PropTypes.func,
|
||||||
|
onExit: PropTypes.func,
|
||||||
|
onOpened: PropTypes.func,
|
||||||
|
onClosed: PropTypes.func,
|
||||||
|
children: PropTypes.node,
|
||||||
|
className: PropTypes.string,
|
||||||
|
wrapClassName: PropTypes.string,
|
||||||
|
modalClassName: PropTypes.string,
|
||||||
|
backdropClassName: PropTypes.string,
|
||||||
|
contentClassName: PropTypes.string,
|
||||||
|
fade: PropTypes.bool,
|
||||||
|
cssModule: PropTypes.object,
|
||||||
|
zIndex: PropTypes.oneOfType([
|
||||||
|
PropTypes.number,
|
||||||
|
PropTypes.string,
|
||||||
|
]),
|
||||||
|
backdropTransitionTimeout: PropTypes.number,
|
||||||
|
backdropTransitionAppearTimeout: PropTypes.number,
|
||||||
|
backdropTransitionEnterTimeout: PropTypes.number,
|
||||||
|
backdropTransitionLeaveTimeout: PropTypes.number,
|
||||||
|
modalTransitionTimeout: PropTypes.number,
|
||||||
|
modalTransitionAppearTimeout: PropTypes.number,
|
||||||
|
modalTransitionEnterTimeout: PropTypes.number,
|
||||||
|
modalTransitionLeaveTimeout: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
const propsToOmit = Object.keys(propTypes);
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
isOpen: false,
|
||||||
|
autoFocus: true,
|
||||||
|
role: 'dialog',
|
||||||
|
backdrop: true,
|
||||||
|
keyboard: true,
|
||||||
|
zIndex: 1050,
|
||||||
|
fade: true,
|
||||||
|
modalTransitionTimeout: 300,
|
||||||
|
backdropTransitionTimeout: 150,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Modal extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.originalBodyPadding = null;
|
||||||
|
this.isBodyOverflowing = false;
|
||||||
|
this.togglePortal = this.togglePortal.bind(this);
|
||||||
|
this.handleBackdropClick = this.handleBackdropClick.bind(this);
|
||||||
|
this.handleEscape = this.handleEscape.bind(this);
|
||||||
|
this.destroy = this.destroy.bind(this);
|
||||||
|
this.onOpened = this.onOpened.bind(this);
|
||||||
|
this.onClosed = this.onClosed.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.isOpen) {
|
||||||
|
this.togglePortal();
|
||||||
|
}
|
||||||
|
if (this.props.onEnter) {
|
||||||
|
this.props.onEnter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.isOpen !== prevProps.isOpen) {
|
||||||
|
// handle portal events/dom updates
|
||||||
|
this.togglePortal();
|
||||||
|
} else if (this._element) {
|
||||||
|
// rerender portal
|
||||||
|
this.renderIntoSubtree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.destroy();
|
||||||
|
if (this.props.onExit) {
|
||||||
|
this.props.onExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened() {
|
||||||
|
if (this.props.onOpened) {
|
||||||
|
this.props.onOpened();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed() {
|
||||||
|
this.destroy();
|
||||||
|
if (this.props.onClosed) {
|
||||||
|
this.props.onClosed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEscape(e) {
|
||||||
|
if (this.props.keyboard && e.keyCode === 27 && this.props.toggle) {
|
||||||
|
this.props.toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBackdropClick(e) {
|
||||||
|
if (this.props.backdrop !== true) return;
|
||||||
|
|
||||||
|
const container = this._dialog;
|
||||||
|
|
||||||
|
if (e.target && !container.contains(e.target) && this.props.toggle) {
|
||||||
|
this.props.toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasTransition() {
|
||||||
|
if (this.props.fade === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.modalTransitionTimeout > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePortal() {
|
||||||
|
if (this.props.isOpen) {
|
||||||
|
if (this.props.autoFocus) {
|
||||||
|
this._focus = true;
|
||||||
|
}
|
||||||
|
this.show();
|
||||||
|
if (!this.hasTransition()) {
|
||||||
|
this.onOpened();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.hide();
|
||||||
|
if (!this.hasTransition()) {
|
||||||
|
this.onClosed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
if (this._element) {
|
||||||
|
ReactDOM.unmountComponentAtNode(this._element);
|
||||||
|
document.body.removeChild(this._element);
|
||||||
|
this._element = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use regex to prevent matching `modal-open` as part of a different class, e.g. `my-modal-opened`
|
||||||
|
const classes = document.body.className.replace(/(^| )modal-open( |$)/, ' ');
|
||||||
|
document.body.className = mapToCssModules(classNames(classes).trim(), this.props.cssModule);
|
||||||
|
setScrollbarWidth(this.originalBodyPadding);
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
this.renderIntoSubtree();
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
const classes = document.body.className;
|
||||||
|
this._element = document.createElement('div');
|
||||||
|
this._element.setAttribute('tabindex', '-1');
|
||||||
|
this._element.style.position = 'relative';
|
||||||
|
this._element.style.zIndex = this.props.zIndex;
|
||||||
|
this.originalBodyPadding = getOriginalBodyPadding();
|
||||||
|
|
||||||
|
conditionallyUpdateScrollbar();
|
||||||
|
|
||||||
|
document.body.appendChild(this._element);
|
||||||
|
|
||||||
|
document.body.className = mapToCssModules(classNames(
|
||||||
|
classes,
|
||||||
|
'modal-open'
|
||||||
|
), this.props.cssModule);
|
||||||
|
|
||||||
|
this.renderIntoSubtree();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderModalDialog() {
|
||||||
|
const attributes = omit(this.props, propsToOmit);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={mapToCssModules(classNames('modal-dialog', this.props.className, {
|
||||||
|
[`modal-${this.props.size}`]: this.props.size
|
||||||
|
}), this.props.cssModule)}
|
||||||
|
role="document"
|
||||||
|
ref={(c) => (this._dialog = c)}
|
||||||
|
{...attributes}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={mapToCssModules(
|
||||||
|
classNames('modal-content', this.props.contentClassName),
|
||||||
|
this.props.cssModule
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderIntoSubtree() {
|
||||||
|
ReactDOM.unstable_renderSubtreeIntoContainer(
|
||||||
|
this,
|
||||||
|
this.renderChildren(),
|
||||||
|
this._element
|
||||||
|
);
|
||||||
|
|
||||||
|
// check if modal should receive focus
|
||||||
|
if (this._focus) {
|
||||||
|
this._dialog.parentNode.focus();
|
||||||
|
this._focus = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChildren() {
|
||||||
|
const {
|
||||||
|
wrapClassName,
|
||||||
|
modalClassName,
|
||||||
|
backdropClassName,
|
||||||
|
cssModule,
|
||||||
|
isOpen,
|
||||||
|
backdrop,
|
||||||
|
modalTransitionTimeout,
|
||||||
|
backdropTransitionTimeout,
|
||||||
|
role,
|
||||||
|
labelledBy
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const modalAttributes = {
|
||||||
|
onClickCapture: this.handleBackdropClick,
|
||||||
|
onKeyUp: this.handleEscape,
|
||||||
|
style: { display: 'block' },
|
||||||
|
'aria-labelledby': labelledBy,
|
||||||
|
role,
|
||||||
|
tabIndex: '-1'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.hasTransition()) {
|
||||||
|
return (
|
||||||
|
<TransitionGroup component="div" className={mapToCssModules(wrapClassName)}>
|
||||||
|
{isOpen && (
|
||||||
|
<Fade
|
||||||
|
key="modal-dialog"
|
||||||
|
onEnter={this.onOpened}
|
||||||
|
onLeave={this.onClosed}
|
||||||
|
transitionAppearTimeout={
|
||||||
|
typeof this.props.modalTransitionAppearTimeout === 'number'
|
||||||
|
? this.props.modalTransitionAppearTimeout
|
||||||
|
: modalTransitionTimeout
|
||||||
|
}
|
||||||
|
transitionEnterTimeout={
|
||||||
|
typeof this.props.modalTransitionEnterTimeout === 'number'
|
||||||
|
? this.props.modalTransitionEnterTimeout
|
||||||
|
: modalTransitionTimeout
|
||||||
|
}
|
||||||
|
transitionLeaveTimeout={
|
||||||
|
typeof this.props.modalTransitionLeaveTimeout === 'number'
|
||||||
|
? this.props.modalTransitionLeaveTimeout
|
||||||
|
: modalTransitionTimeout
|
||||||
|
}
|
||||||
|
cssModule={cssModule}
|
||||||
|
className={mapToCssModules(classNames('modal', modalClassName), cssModule)}
|
||||||
|
{...modalAttributes}
|
||||||
|
>
|
||||||
|
{this.renderModalDialog()}
|
||||||
|
</Fade>
|
||||||
|
)}
|
||||||
|
{isOpen && backdrop && (
|
||||||
|
<Fade
|
||||||
|
key="modal-backdrop"
|
||||||
|
transitionAppearTimeout={
|
||||||
|
typeof this.props.backdropTransitionAppearTimeout === 'number'
|
||||||
|
? this.props.backdropTransitionAppearTimeout
|
||||||
|
: backdropTransitionTimeout
|
||||||
|
}
|
||||||
|
transitionEnterTimeout={
|
||||||
|
typeof this.props.backdropTransitionEnterTimeout === 'number'
|
||||||
|
? this.props.backdropTransitionEnterTimeout
|
||||||
|
: backdropTransitionTimeout
|
||||||
|
}
|
||||||
|
transitionLeaveTimeout={
|
||||||
|
typeof this.props.backdropTransitionLeaveTimeout === 'number'
|
||||||
|
? this.props.backdropTransitionLeaveTimeout
|
||||||
|
: backdropTransitionTimeout
|
||||||
|
}
|
||||||
|
cssModule={cssModule}
|
||||||
|
className={mapToCssModules(classNames('modal-backdrop', backdropClassName), cssModule)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TransitionGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={mapToCssModules(wrapClassName)}>
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
className={mapToCssModules(classNames('modal', 'show', modalClassName), cssModule)}
|
||||||
|
{...modalAttributes}
|
||||||
|
>
|
||||||
|
{this.renderModalDialog()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isOpen && backdrop && (
|
||||||
|
<div
|
||||||
|
className={mapToCssModules(classNames('modal-backdrop', 'show', backdropClassName), cssModule)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.propTypes = propTypes;
|
||||||
|
Modal.defaultProps = defaultProps;
|
||||||
|
|
||||||
|
export default Modal;
|
|
@ -7,40 +7,42 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import React from "react";
|
import {h, Component} from "preact"; /** @jsx h*/
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Remember to install codemirror before react-codemirror,
|
Remember to install codemirror before react-codemirror,
|
||||||
see issue https://github.com/JedWatson/react-codemirror/issues/34
|
see issue https://github.com/JedWatson/react-codemirror/issues/34
|
||||||
Also add in the themes in the CSS to get proper theming support.
|
Also add in the themes in the CSS to get proper theming support.
|
||||||
*/
|
*/
|
||||||
import CodeMirror from "react-codemirror";
|
import CodeMirror from "react-codemirror2";
|
||||||
// TODO: Figure out how to work around this import.
|
// TODO: Figure out how to work around this import.
|
||||||
|
|
||||||
// Workaround for rendering CodeMirror server-side
|
// Workaround for rendering CodeMirror server-side
|
||||||
if (typeof(navigator) !== 'undefined') {
|
if (typeof(navigator) !== 'undefined') {
|
||||||
require("codemirror/mode/markdown/markdown");
|
require("codemirror/mode/markdown/markdown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CodeEditor extends React.Component {
|
class CodeEditor extends Component {
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
return {
|
return {
|
||||||
code: React.PropTypes.string,
|
code: PropTypes.string,
|
||||||
language: React.PropTypes.string,
|
language: PropTypes.string,
|
||||||
theme: React.PropTypes.string,
|
theme: PropTypes.string,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let code = this.props.code;
|
let code = this.props.code;
|
||||||
let options = {mode: this.props.language, theme: this.props.theme};
|
let options = {mode: this.props.language, theme: this.props.theme};
|
||||||
|
|
||||||
// TODO: Improve workaround when rendering React CodeMirror on the server.
|
// TODO: Improve workaround when rendering React CodeMirror on the server.
|
||||||
// TODO: Add patch to resolve issue at https://github.com/JedWatson/react-codemirror/issues
|
// TODO: Add patch to resolve issue at https://github.com/JedWatson/react-codemirror/issues
|
||||||
// TODO: Resolve issues with rendering Code Mirror componnets.
|
// TODO: Resolve issues with rendering Code Mirror componnets.
|
||||||
return (<CodeMirror value={ code } onChange={ this.handleUpdateCode } options={ options }/>);
|
return (<CodeMirror value={ code } onChange={ this.handleUpdateCode } options={ options }/>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,173 +7,191 @@ Markdown controller to view and update journal entries.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import React from "react";
|
import {h, Component} from "preact"; /** @jsx h*/
|
||||||
import {connect} from "react-redux";
|
|
||||||
|
|
||||||
import marked from "marked";
|
import {connect} from "preact-redux";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import markdown from "markdown-it";
|
||||||
import highlighter from "highlight.js";
|
import highlighter from "highlight.js";
|
||||||
import {Button, Panel} from "react-bootstrap";
|
import {Button, Collapse} from "reactstrap";
|
||||||
import FontAwesome from "react-fontawesome";
|
import FontAwesome from "react-fontawesome";
|
||||||
|
|
||||||
import {fetchPage, savePage} from '../actions';
|
import {fetchPage, savePage} from '../actions';
|
||||||
import {appStore} from "../stores";
|
import {appStore} from "../stores";
|
||||||
|
import {withRouter} from 'react-router-dom';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Remember to install codemirror before react-codemirror,
|
Remember to install codemirror before react-codemirror,
|
||||||
see issue https://github.com/JedWatson/react-codemirror/issues/34
|
see issue https://github.com/JedWatson/react-codemirror/issues/34
|
||||||
Also add in the themes in the CSS to get proper theming support.
|
Also add in the themes in the CSS to get proper theming support.
|
||||||
*/
|
*/
|
||||||
import CodeMirror from "react-codemirror";
|
import CodeMirror from "react-codemirror2";
|
||||||
// TODO: Figure out how to work around this import.
|
// TODO: Figure out how to work around this import.
|
||||||
|
|
||||||
// Workaround for rendering CodeMirror server-side
|
// Workaround for rendering CodeMirror server-side
|
||||||
if (typeof(navigator) !== 'undefined') {
|
if (typeof(navigator) !== 'undefined') {
|
||||||
require("codemirror/mode/markdown/markdown");
|
require("codemirror/mode/markdown/markdown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// # TODO Remove and rework to allow for two flags, rather than a single state?
|
// # TODO Remove and rework to allow for two flags, rather than a single state?
|
||||||
export const EditorPaneState = {
|
export const EditorPaneState = {
|
||||||
hidden: 0,
|
hidden: 0,
|
||||||
editableClosed: 1,
|
editableClosed: 1,
|
||||||
editableOpen: 2
|
editableOpen: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export class JournalMarkdownView extends React.Component {
|
export class JournalMarkdownView extends Component {
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
return {
|
return {
|
||||||
id: React.PropTypes.string,
|
id: PropTypes.string,
|
||||||
article: React.PropTypes.string,
|
article: PropTypes.string,
|
||||||
title: React.PropTypes.string,
|
title: PropTypes.string,
|
||||||
editorTheme: React.PropTypes.string,
|
editorTheme: PropTypes.string,
|
||||||
params: React.PropTypes.shape({
|
location: PropTypes.object.isRequired,
|
||||||
pageId: React.PropTypes.string
|
match: PropTypes.object,
|
||||||
}),
|
userCanEdit: PropTypes.bool
|
||||||
userCanEdit: React.PropTypes.bool
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
displayContent: this.props.article || "Please wait while we load this page's entry.",
|
||||||
|
originalContent: this.props.article || "Please wait while we load this page's entry.",
|
||||||
|
editorPaneState: EditorPaneState.hidden
|
||||||
|
};
|
||||||
|
this.handleSaveContent = this.handleSaveContent.bind(this);
|
||||||
|
this.handleToggleEditorPane = this.handleToggleEditorPane.bind(this);
|
||||||
|
this.handleSaveContent = this.handleSaveContent.bind(this);
|
||||||
|
this.handleResetContent = this.handleResetContent.bind(this);
|
||||||
|
this.handleUpdateCode = this.handleUpdateCode.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
let pageToLoad = this.props.match.params.pageId;
|
||||||
|
appStore.dispatch(fetchPage(pageToLoad));
|
||||||
|
this.updateEditAllowance(this.props.userCanEdit);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
const {match} = this.props;
|
||||||
|
if (match.params.pageId !== nextProps.match.params.pageId) {
|
||||||
|
appStore.dispatch(fetchPage(nextProps.match.params.pageId));
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
displayContent: nextProps.article,
|
||||||
|
originalContent: nextProps.article,
|
||||||
|
});
|
||||||
|
this.updateEditAllowance(nextProps.userCanEdit)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUpdateCode(editor, metadata, newCode) {
|
||||||
|
this.setState({displayContent: newCode});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleToggleEditorPane() {
|
||||||
|
let nextEditorState = this.state.editorPaneState;
|
||||||
|
if (this.state.editorPaneState === EditorPaneState.editableOpen) {
|
||||||
|
nextEditorState = EditorPaneState.editableClosed;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
if (this.state.editorPaneState === EditorPaneState.editableClosed) {
|
||||||
super(props);
|
nextEditorState = EditorPaneState.editableOpen;
|
||||||
this.state = {
|
}
|
||||||
displayContent: this.props.article || "Please wait while we load this pages entry.",
|
this.setState({editorPaneState: nextEditorState});
|
||||||
originalContent: this.props.article || "Please wait while we load this pages entry.",
|
}
|
||||||
editorPaneState: EditorPaneState.hidden
|
|
||||||
};
|
isEditorPaneOpen() {
|
||||||
this.handleSaveContent = this.handleSaveContent.bind(this);
|
return this.state.editorPaneState === EditorPaneState.editableOpen;
|
||||||
this.handleToggleEditorPane = this.handleToggleEditorPane.bind(this);
|
}
|
||||||
this.handleSaveContent = this.handleSaveContent.bind(this);
|
|
||||||
this.handleResetContent = this.handleResetContent.bind(this);
|
handleResetContent() {
|
||||||
this.handleUpdateCode = this.handleUpdateCode.bind(this);
|
this.setState({displayContent: this.state.originalContent});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSaveContent() {
|
||||||
|
// # TODO Start using proper models for page and json
|
||||||
|
let pageContent = {
|
||||||
|
id: this.props.id,
|
||||||
|
title: this.props.title,
|
||||||
|
content: this.state.displayContent
|
||||||
|
};
|
||||||
|
appStore.dispatch(savePage(pageContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEditAllowance(allowEdits) {
|
||||||
|
let editorState = this.state.editorPaneState;
|
||||||
|
if (!allowEdits) {
|
||||||
|
editorState = EditorPaneState.hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
if (allowEdits && this.state.editorPaneState === EditorPaneState.hidden) {
|
||||||
let pageToLoad = this.props.params.pageId;
|
editorState = EditorPaneState.editableClosed;
|
||||||
appStore.dispatch(fetchPage(pageToLoad));
|
}
|
||||||
this.updateEditAllowance(this.props.userCanEdit);
|
this.setState({editorPaneState: editorState});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let code = this.state.displayContent;
|
||||||
|
let md = markdown({
|
||||||
|
html: true,
|
||||||
|
highlight: (code) => {
|
||||||
|
return highlighter.highlightAuto(code).value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let rawMarkup = md.render(code);
|
||||||
|
|
||||||
|
// # TODO Factor the view into something nicer.
|
||||||
|
let showEditorButton = (
|
||||||
|
<Button onClick={ this.handleToggleEditorPane }>
|
||||||
|
<FontAwesome name="edit"/> Edit
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
// # TODO Add in ability to display mhessages outside the collapsible pane
|
||||||
|
let showPanel = (
|
||||||
|
<Collapse isOpen={ this.isEditorPaneOpen() }>
|
||||||
|
<CodeMirror
|
||||||
|
value={ code }
|
||||||
|
onChange={ this.handleUpdateCode }
|
||||||
|
options={{ mode: "markdown", theme: this.props.editorTheme }} />
|
||||||
|
<Button onClick={ this.handleSaveContent }>
|
||||||
|
<FontAwesome name="save"/> Save
|
||||||
|
</Button>
|
||||||
|
<Button onClick={ this.handleResetContent }>
|
||||||
|
<FontAwesome name="undo"/> Reset
|
||||||
|
</Button>
|
||||||
|
</Collapse>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.state.editorPaneState === EditorPaneState.hidden) {
|
||||||
|
showEditorButton = (<div />);
|
||||||
|
showPanel = (<div />);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
return (
|
||||||
this.setState({displayContent: nextProps.article, originalContent: nextProps.article});
|
<div>
|
||||||
this.updateEditAllowance(nextProps.userCanEdit)
|
<h1>{ this.props.title }</h1>
|
||||||
}
|
<span dangerouslySetInnerHTML={{ __html: rawMarkup }} />
|
||||||
|
{ showEditorButton }
|
||||||
handleUpdateCode(newCode) {
|
{ showPanel }
|
||||||
this.setState({displayContent: newCode});
|
</div>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
handleToggleEditorPane() {
|
|
||||||
let nextEditorState = this.state.editorPaneState;
|
|
||||||
if (this.state.editorPaneState === EditorPaneState.editableOpen) {
|
|
||||||
nextEditorState = EditorPaneState.editableClosed;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.editorPaneState === EditorPaneState.editableClosed) {
|
|
||||||
nextEditorState = EditorPaneState.editableOpen;
|
|
||||||
}
|
|
||||||
this.setState({editorPaneState: nextEditorState});
|
|
||||||
}
|
|
||||||
|
|
||||||
isEditorPaneOpen() {
|
|
||||||
return this.state.editorPaneState === EditorPaneState.editableOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResetContent() {
|
|
||||||
this.setState({displayContent: this.state.originalContent});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSaveContent() {
|
|
||||||
// # TODO Start using proper models for page and json
|
|
||||||
let pageContent = {
|
|
||||||
id: this.props.id,
|
|
||||||
title: this.props.title,
|
|
||||||
content: this.state.displayContent
|
|
||||||
};
|
|
||||||
appStore.dispatch(savePage(pageContent));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateEditAllowance(allowEdits) {
|
|
||||||
let editorState = this.state.editorPaneState;
|
|
||||||
if (!allowEdits) {
|
|
||||||
editorState = EditorPaneState.hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowEdits && this.state.editorPaneState === EditorPaneState.hidden) {
|
|
||||||
editorState = EditorPaneState.editableClosed;
|
|
||||||
}
|
|
||||||
this.setState({editorPaneState: editorState});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let code = this.state.displayContent;
|
|
||||||
let rawMarkup = marked(code, {sanitized: true, highlight: (code) => {
|
|
||||||
return highlighter.highlightAuto(code).value;
|
|
||||||
}});
|
|
||||||
|
|
||||||
// # TODO Factor the view into something nicer.
|
|
||||||
let showEditorButton = (
|
|
||||||
<Button onClick={ this.handleToggleEditorPane }>
|
|
||||||
<FontAwesome name="edit"/> Edit
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
let editorOptions = {
|
|
||||||
mode: "markdown",
|
|
||||||
theme: this.props.editorTheme
|
|
||||||
};
|
|
||||||
|
|
||||||
// # TODO Add in ability to display mhessages outside the collapsible pane
|
|
||||||
let showPanel = (
|
|
||||||
<Panel collapsible expanded={ this.isEditorPaneOpen() }>
|
|
||||||
<CodeMirror value={ code } onChange={ this.handleUpdateCode } options={ editorOptions } />
|
|
||||||
<Button onClick={ this.handleSaveContent }><FontAwesome name="save"/> Save </Button>
|
|
||||||
<Button onClick={ this.handleResetContent }><FontAwesome name="undo"/> Reset </Button>
|
|
||||||
</Panel>
|
|
||||||
);
|
|
||||||
if (this.state.editorPaneState === EditorPaneState.hidden) {
|
|
||||||
showEditorButton = (<div />);
|
|
||||||
showPanel = (<div />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>{ this.props.title }</h1>
|
|
||||||
<span dangerouslySetInnerHTML={ {__html: rawMarkup} }/>
|
|
||||||
{ showEditorButton }
|
|
||||||
{ showPanel }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectedArticle = connect(
|
export const ConnectedArticle = withRouter(
|
||||||
|
connect(
|
||||||
(state) => {
|
(state) => {
|
||||||
return {
|
return {
|
||||||
'id': state.page.id,
|
'id': state.page.id,
|
||||||
'article': state.page.content,
|
'article': state.page.content,
|
||||||
'title': state.page.title,
|
'title': state.page.title,
|
||||||
'editorTheme': state.editor.theme,
|
'editorTheme': state.editor.theme,
|
||||||
'userCanEdit': state.user.token !== ""
|
'userCanEdit': state.user.token !== ""
|
||||||
};
|
};
|
||||||
})(JournalMarkdownView);
|
})(JournalMarkdownView)
|
||||||
|
);
|
||||||
|
|
|
@ -7,63 +7,71 @@ Navigation Menu
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import React from "react";
|
import {h, Component} from "preact"; /** @jsx h*/
|
||||||
import {Link} from "react-router";
|
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import {Col, Nav, NavItem, NavLink} from "reactstrap";
|
||||||
|
|
||||||
|
|
||||||
class NavigationMenuItem extends React.Component {
|
class NavigationMenuItem extends Component {
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
return {
|
return {
|
||||||
title: React.PropTypes.string,
|
title: PropTypes.string,
|
||||||
url: React.PropTypes.string
|
url: PropTypes.string
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (<li><Link to={ this.props.url }>{ this.props.title }</Link></li>);
|
return (
|
||||||
}
|
<NavItem>
|
||||||
|
<NavLink>
|
||||||
|
<Link to={ this.props.url }>{ this.props.title }</Link>
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class NavigationMenu extends React.Component {
|
class NavigationMenu extends Component {
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
return {
|
return {
|
||||||
menu: React.PropTypes.arrayOf(
|
menu: PropTypes.arrayOf(
|
||||||
React.PropTypes.shape({
|
PropTypes.shape({
|
||||||
title: React.PropTypes.string,
|
title: PropTypes.string,
|
||||||
url: React.PropTypes.url
|
url: PropTypes.url
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let navigationMenuItems = [];
|
||||||
|
for (let menuItem of this.props.menu) {
|
||||||
|
let menuId = `${menuItem.title.toLowerCase()}_${menuItem.url.substring(1, menuItem.url.length)}`;
|
||||||
|
// TODO Fix API for application menu.
|
||||||
|
let menu = (
|
||||||
|
<NavigationMenuItem key={ menuId } title={ menuItem.title } url={ menuItem.url }/>
|
||||||
|
);
|
||||||
|
navigationMenuItems.push(menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
let navigationMenuItems = [];
|
<Col lg="3">
|
||||||
for (let menuItem of this.props.menu) {
|
<Nav pills vertical className="ice-menu">
|
||||||
let menuId = `${menuItem.title.toLowerCase()}_${menuItem.url.substring(1, menuItem.url.length)}`;
|
{ navigationMenuItems }
|
||||||
// TODO Fix API for application menu.
|
</Nav>
|
||||||
let menu = (
|
</Col>
|
||||||
<NavigationMenuItem key={ menuId } title={ menuItem.title } url={ menuItem.url }/>
|
);
|
||||||
);
|
}
|
||||||
navigationMenuItems.push(menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="col-lg-2 col-lg-offset-1 ice-menu">
|
|
||||||
<nav>
|
|
||||||
<ul className="nav nav-stacked nav-pills">{ navigationMenuItems }</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ConnectedNavigationMenu = connect(
|
const ConnectedNavigationMenu = connect(
|
||||||
(state) => {
|
(state) => {
|
||||||
return {'menu': state.menu};
|
return {'menu': state.menu};
|
||||||
}
|
}
|
||||||
)(NavigationMenu);
|
)(NavigationMenu);
|
||||||
|
|
||||||
export {ConnectedNavigationMenu};
|
export {ConnectedNavigationMenu};
|
|
@ -7,23 +7,26 @@ Site footer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import React from "react";
|
import {h, Component} from "preact"; /** @jsx h*/
|
||||||
|
import {Row, Col} from "reactstrap";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
|
||||||
export class SiteFooter extends React.Component {
|
export class SiteFooter extends Component {
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
return {
|
return {
|
||||||
footer: React.PropTypes.string
|
footer: PropTypes.string
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<Row className="justify-content-end">
|
||||||
<div className="col-lg-6 col-lg-offset-3">
|
<Col lg="8">
|
||||||
<footer>{ this.props.footer }</footer>
|
<footer>{ this.props.footer }</footer>
|
||||||
</div>
|
</Col>
|
||||||
</div>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,35 +6,38 @@ Site header
|
||||||
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import {h, Component} from "preact"; /** @jsx h*/
|
||||||
|
import {Row, Col} from "reactstrap";
|
||||||
|
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
|
||||||
export class SiteHeader extends React.Component {
|
export class SiteHeader extends Component {
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
return {
|
return {
|
||||||
siteName: React.PropTypes.string,
|
siteName: PropTypes.string,
|
||||||
logo: React.PropTypes.string,
|
logo: PropTypes.string,
|
||||||
tagline: React.PropTypes.string
|
tagline: PropTypes.string
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let headerLogoAltText = `${this.props.siteName} logo`;
|
let headerLogoAltText = `${this.props.siteName} logo`;
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<Row className="justify-content-around">
|
||||||
<div className="col-lg-10 col-lg-offset-1">
|
<Col lg="11">
|
||||||
<div className="row sea-header">
|
<Row className="sea-header justify-content-center align-items-center">
|
||||||
<div className="col-lg-2 col-lg-offset-3">
|
<Col lg="2">
|
||||||
<img className="header_logo" src={ this.props.logo } alt={ headerLogoAltText }/>
|
<img className="header_logo" src={ this.props.logo } alt={ headerLogoAltText }/>
|
||||||
</div>
|
</Col>
|
||||||
<div className="col-lg-4">
|
<Col lg="4">
|
||||||
<h1 className="header_title">{ this.props.siteName }</h1>
|
<h1 className="header_title">{ this.props.siteName }</h1>
|
||||||
|
|
||||||
<h2 className="header_tagline">{ this.props.tagline }</h2>
|
<h2 className="header_tagline">{ this.props.tagline }</h2>
|
||||||
</div>
|
</Col>
|
||||||
</div>
|
</Row>
|
||||||
</div>
|
</Col>
|
||||||
</div>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,41 +6,43 @@ Switches between premade themes for the site.
|
||||||
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import {h, Component} from "preact"; /** @jsx h*/
|
||||||
|
|
||||||
import FontAwesome from "react-fontawesome";
|
import FontAwesome from "react-fontawesome";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import {appStore} from "../stores";
|
import {appStore} from "../stores";
|
||||||
import {switchThemes} from "../actions";
|
import {switchThemes} from "../actions";
|
||||||
|
|
||||||
|
|
||||||
export class ThemeSwitchView extends React.Component {
|
export class ThemeSwitchView extends Component {
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
return {
|
return {
|
||||||
theme: React.PropTypes.string,
|
theme: PropTypes.string,
|
||||||
alternativeTheme: React.PropTypes.string
|
alternativeTheme: PropTypes.string
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSwitchTheme() {
|
handleSwitchTheme() {
|
||||||
appStore.dispatch(switchThemes());
|
appStore.dispatch(switchThemes());
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let changeThemeText = `Change to ${this.props.alternativeTheme} theme`;
|
let changeThemeText = `Change to ${this.props.alternativeTheme} theme`;
|
||||||
return (
|
return (
|
||||||
<div className="text-center muted hoverable" onClick={ this.handleSwitchTheme }>
|
<div className="text-center muted hoverable" onClick={ this.handleSwitchTheme }>
|
||||||
<FontAwesome name="eye" />
|
<FontAwesome name="eye" />
|
||||||
{ changeThemeText }
|
{ changeThemeText }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConnectedThemeSwitchView = connect(
|
export const ConnectedThemeSwitchView = connect(
|
||||||
(state) => {
|
(state) => {
|
||||||
return {
|
return {
|
||||||
'theme': state.theme.theme,
|
'theme': state.theme.theme,
|
||||||
'alternativeTheme': state.theme.alternativeTheme
|
'alternativeTheme': state.theme.alternativeTheme
|
||||||
};
|
};
|
||||||
})(ThemeSwitchView);
|
})(ThemeSwitchView);
|
||||||
|
|
|
@ -6,168 +6,172 @@ User login and logout controller
|
||||||
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable react/jsx-handler-names */
|
||||||
|
|
||||||
import React from "react";
|
import {h, Component} from "preact"; /** @jsx h*/
|
||||||
import {Modal, Button, Alert, FormControl, Form, ControlLabel, FormGroup, Col} from "react-bootstrap";
|
|
||||||
|
import {ModalBody, ModalFooter, ModalHeader, Button, Alert, Input, Form, Label, FormGroup, Col} from "reactstrap";
|
||||||
|
import Modal from "../vendor/Modal";
|
||||||
import FontAwesome from "react-fontawesome";
|
import FontAwesome from "react-fontawesome";
|
||||||
import {connect} from "react-redux";
|
import {connect} from "react-redux";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import {loginUser, logoutUser} from "../actions";
|
import {loginUser, logoutUser} from "../actions";
|
||||||
import {appStore} from "../stores";
|
import {appStore} from "../stores";
|
||||||
|
|
||||||
|
|
||||||
export class UserLoginView extends React.Component {
|
export class UserLoginView extends Component {
|
||||||
static get propTypes() {
|
static get propTypes() {
|
||||||
return {
|
return {
|
||||||
isUserLoggedIn: React.PropTypes.bool,
|
isUserLoggedIn: PropTypes.bool,
|
||||||
fullName: React.PropTypes.string,
|
fullName: PropTypes.string,
|
||||||
externalError: React.PropTypes.string,
|
externalError: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
errorMessage: "",
|
||||||
|
displayLoginModal: false
|
||||||
|
};
|
||||||
|
this.handleLoginAttempt = this.handleLoginAttempt.bind(this);
|
||||||
|
this.updateErrorMessage = this.updateErrorMessage.bind(this);
|
||||||
|
this.handleShowLoginModal = this.handleShowLoginModal.bind(this);
|
||||||
|
this.handleHideLoginModal = this.handleHideLoginModal.bind(this);
|
||||||
|
this.handleLogoutUser = this.handleLogoutUser.bind(this);
|
||||||
|
this.handleUsernameUpdate = this.handleUsernameUpdate.bind(this);
|
||||||
|
this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this);
|
||||||
|
this.handleKeyboardLogin = this.handleKeyboardLogin.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
// TODO: Fix the transitions up, maybe have the login state as part of the app itself.
|
||||||
|
if (nextProps.externalError !== "") {
|
||||||
|
this.updateErrorMessage(nextProps.externalError);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
if (nextProps.isUserLoggedIn === true) {
|
||||||
super(props);
|
this.handleHideLoginModal();
|
||||||
this.state = {
|
}
|
||||||
username: "",
|
}
|
||||||
password: "",
|
|
||||||
errorMessage: "",
|
handleLoginAttempt() {
|
||||||
displayLoginModal: false
|
if (this.state.username === "" || this.state.password === "") {
|
||||||
};
|
this.updateErrorMessage("Enter a username and password to login in...");
|
||||||
this.handleLoginAttempt = this.handleLoginAttempt.bind(this);
|
return;
|
||||||
this.updateErrorMessage = this.updateErrorMessage.bind(this);
|
}
|
||||||
this.handleShowLoginModal = this.handleShowLoginModal.bind(this);
|
appStore.dispatch(loginUser(this.state.username, this.state.password));
|
||||||
this.handleHideLoginModal = this.handleHideLoginModal.bind(this);
|
}
|
||||||
this.handleLogoutUser = this.handleLogoutUser.bind(this);
|
|
||||||
this.handleUsernameUpdate = this.handleUsernameUpdate.bind(this);
|
updateErrorMessage(errorMessage="") {
|
||||||
this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this);
|
this.setState({errorMessage: errorMessage});
|
||||||
this.handleKeyboardLogin = this.handleKeyboardLogin.bind(this);
|
}
|
||||||
|
|
||||||
|
handleShowLoginModal() {
|
||||||
|
this.setState({displayLoginModal: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleHideLoginModal() {
|
||||||
|
this.setState({username: "", password: "", errorMessage: "", displayLoginModal: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLogoutUser() {
|
||||||
|
appStore.dispatch(logoutUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUsernameUpdate(event) {
|
||||||
|
this.setState({username: event.target.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePasswordUpdate(event) {
|
||||||
|
this.setState({password: event.target.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyboardLogin(event) {
|
||||||
|
if (!this.state.displayLoginModal || event.key !== "Enter") {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
if (this.state.username === "" || this.state.password === "") {
|
||||||
// TODO: Fix the transitions up, maybe have the login state as part of the app itself.
|
this.updateErrorMessage("Enter a username and password to login in...");
|
||||||
if (nextProps.externalError !== "") {
|
return;
|
||||||
this.updateErrorMessage(nextProps.externalError);
|
}
|
||||||
}
|
appStore.dispatch(loginUser(this.state.username, this.state.password));
|
||||||
|
}
|
||||||
|
|
||||||
if (nextProps.isUserLoggedIn === true) {
|
render() {
|
||||||
this.handleHideLoginModal();
|
let loggedIn = this.props.isUserLoggedIn;
|
||||||
}
|
let loginMessage = loggedIn ? "logout" : "login";
|
||||||
|
let loginIcon = loggedIn ? (<FontAwesome name="sign-in"/>) : (<FontAwesome name="sign-out"/>);
|
||||||
|
let loginOperation = loggedIn ? this.handleLogoutUser : this.handleShowLoginModal;
|
||||||
|
|
||||||
|
let errorDisplay = "";
|
||||||
|
if (this.state.errorMessage !== undefined && this.state.errorMessage !== "") {
|
||||||
|
errorDisplay = (<Alert color="danger">{ this.state.errorMessage }</Alert>);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoginAttempt() {
|
let loginButtonDisabled = this.state.username === "" || this.state.password === "";
|
||||||
if (this.state.username === "" || this.state.password === "") {
|
|
||||||
this.updateErrorMessage("Enter a username and password to login in...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
appStore.dispatch(loginUser(this.state.username, this.state.password));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateErrorMessage(errorMessage="") {
|
return (
|
||||||
this.setState({errorMessage: errorMessage});
|
<div>
|
||||||
}
|
<div className="text-center muted hoverable" onClick={ loginOperation }>
|
||||||
|
<FontAwesome name="user"/>
|
||||||
handleShowLoginModal() {
|
|
||||||
this.setState({displayLoginModal: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleHideLoginModal() {
|
|
||||||
this.setState({username: "", password: "", errorMessage: "", displayLoginModal: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLogoutUser() {
|
|
||||||
appStore.dispatch(logoutUser());
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUsernameUpdate(event) {
|
|
||||||
this.setState({username: event.target.value});
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePasswordUpdate(event) {
|
|
||||||
this.setState({password: event.target.value});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyboardLogin(event) {
|
|
||||||
if (!this.state.displayLoginModal || event.key !== "Enter") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.username === "" || this.state.password === "") {
|
|
||||||
this.updateErrorMessage("Enter a username and password to login in...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
appStore.dispatch(loginUser(this.state.username, this.state.password));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let loggedIn = this.props.isUserLoggedIn;
|
|
||||||
let loginMessage = loggedIn ? "logout" : "login";
|
|
||||||
let loginIcon = loggedIn ? (<FontAwesome name="sign-in"/>) : (<FontAwesome name="sign-out"/>);
|
|
||||||
let loginOperation = loggedIn ? this.handleLogoutUser : this.handleShowLoginModal;
|
|
||||||
|
|
||||||
let errorDisplay = "";
|
|
||||||
if (this.state.errorMessage !== undefined && this.state.errorMessage !== "") {
|
|
||||||
errorDisplay = (<Alert bsStyle="danger">{ this.state.errorMessage }</Alert>);
|
|
||||||
}
|
|
||||||
|
|
||||||
let loginButtonDisabled = this.state.username === "" || this.state.password === "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="text-center muted hoverable" onClick={ loginOperation }>
|
|
||||||
<FontAwesome name="user"/>
|
|
||||||
|
|
||||||
Hello { this.props.fullName }!
|
Hello { this.props.fullName }!
|
||||||
 
|
 
|
||||||
{ loginIcon }
|
{ loginIcon }
|
||||||
|
|
||||||
Do you want to { loginMessage }?
|
Do you want to { loginMessage }?
|
||||||
</div>
|
</div>
|
||||||
<Modal show={ this.state.displayLoginModal } onHide={ this.handleHideLoginModal } onKeyPress={ this.handleKeyboardLogin }>
|
<Modal isOpen={ this.state.displayLoginModal } toggle={ this.handleHideLoginModal } onKeyPress={ this.handleKeyboardLogin }>
|
||||||
<Modal.Header closeButton>
|
<ModalHeader toggle={ this.handleHideLoginModal }>
|
||||||
<Modal.Title>User Login</Modal.Title>
|
User Login
|
||||||
</Modal.Header>
|
</ModalHeader>
|
||||||
|
|
||||||
<Modal.Body>
|
<ModalBody>
|
||||||
{errorDisplay}
|
{errorDisplay}
|
||||||
<Form horizontal>
|
<Form horizontal>
|
||||||
<FormGroup controlId="username">
|
<FormGroup row>
|
||||||
<Col xs={ 2 }>
|
<Col xs="2">
|
||||||
<ControlLabel>Username</ControlLabel>
|
<Label for="username">Username</Label>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={ 10 }>
|
<Col xs="10" >
|
||||||
<FormControl type="text" placeholder="Username"
|
<Input id="username" type="text" placeholder="Username"
|
||||||
value={ this.state.username } onChange={ this.handleUsernameUpdate } />
|
value={ this.state.username } onChange={ this.handleUsernameUpdate } />
|
||||||
</Col>
|
</Col>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormGroup controlId="password">
|
<FormGroup row>
|
||||||
<Col xs={ 2 }>
|
<Col xs="2">
|
||||||
<ControlLabel>Password</ControlLabel>
|
<Label for="password">Password</Label>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={ 10 }>
|
<Col xs="10">
|
||||||
<FormControl type="password" placeholder="***"
|
<Input id="password" type="password" placeholder="***"
|
||||||
value={ this.state.password } onChange={ this.handlePasswordUpdate } />
|
value={ this.state.password } onChange={ this.handlePasswordUpdate } />
|
||||||
</Col>
|
</Col>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal.Body>
|
</ModalBody>
|
||||||
|
|
||||||
<Modal.Footer>
|
<ModalFooter>
|
||||||
<Button bsStyle="warning" onClick={ this.handleHideLoginModal }>Cancel</Button>
|
<Button color="warning" onClick={ this.handleHideLoginModal }>Cancel</Button>
|
||||||
<Button onClick={ this.handleLoginAttempt } disabled={ loginButtonDisabled }>Login</Button>
|
<Button color="primary" onClick={ this.handleLoginAttempt } disabled={ loginButtonDisabled }>Login</Button>
|
||||||
</Modal.Footer>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const ConnectedUserLogin = connect(
|
export const ConnectedUserLogin = connect(
|
||||||
(state) => {
|
(state) => {
|
||||||
return {
|
return {
|
||||||
"fullName": state.user.fullName,
|
"fullName": state.user.profile !== undefined ? state.user.profile.fullName : 'Stranger',
|
||||||
"isUserLoggedIn": state.user.token !== "",
|
"isUserLoggedIn": state.user.token !== "",
|
||||||
"externalError": state.user.accountState
|
"externalError": state.user.accountState
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)(UserLoginView);
|
)(UserLoginView);
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
/* Bootstrap */
|
||||||
|
@import url("~bootstrap/dist/css/bootstrap.min.css");
|
||||||
|
|
||||||
|
/* Font-Awesome */
|
||||||
|
@import url("~font-awesome/css/font-awesome.css");
|
||||||
|
|
||||||
|
/* Code Mirror styles */
|
||||||
|
@import url("~codemirror/lib/codemirror.css");
|
||||||
|
@import url("~codemirror/theme/monokai.css");
|
||||||
|
|
||||||
|
/* Highlight JS */
|
||||||
|
@import url("~highlight.js/styles/default.css");
|
||||||
|
@import url("~highlight.js/styles/monokai.css");
|
|
@ -1,11 +0,0 @@
|
||||||
// Bootstrap
|
|
||||||
@import (inline) "../../node_modules/bootstrap/dist/css/bootstrap.css";
|
|
||||||
@import (inline) "../../node_modules/bootstrap/dist/css/bootstrap-theme.css";
|
|
||||||
|
|
||||||
// Code Mirror styles
|
|
||||||
@import (inline) "../../node_modules/codemirror/lib/codemirror.css";
|
|
||||||
@import (inline) "../../node_modules/codemirror/theme/monokai.css";
|
|
||||||
|
|
||||||
// Highlight JS
|
|
||||||
@import (inline) "../../node_modules/highlight.js/styles/default.css";
|
|
||||||
@import (inline) "../../node_modules/highlight.js/styles/monokai.css";
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,9 +7,8 @@
|
||||||
* @version: 0.5.0
|
* @version: 0.5.0
|
||||||
* @license: AGPL v3
|
* @license: AGPL v3
|
||||||
*/
|
*/
|
||||||
// Bootstrap - Needs to be imported first.
|
/* Bootstrap - Needs to be imported first. */
|
||||||
@import (inline) "../../node_modules/bootstrap/dist/css/bootstrap.css";
|
@import url("~bootstrap/dist/css/bootstrap.css");
|
||||||
@import (inline) "../../node_modules/bootstrap/dist/css/bootstrap-theme.css";
|
|
||||||
|
|
||||||
/*** Declare font-faces from openfontlibrary ***/
|
/*** Declare font-faces from openfontlibrary ***/
|
||||||
@font-face {
|
@font-face {
|
||||||
|
@ -21,7 +20,7 @@
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'LavoirSmallCaps';
|
font-family: 'LavoirSmallCaps';
|
||||||
src: url('/static/fonts/openfonts/lavoir/Lavoir%20Small%20Caps.otf') format('opentype');
|
src: url('/static/fonts/openfonts/lavoir/Lavoir_Small_Caps.otf') format('opentype');
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
@ -48,35 +47,47 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** Fonts ***/
|
/*** Fonts ***/
|
||||||
@header-title-font-family: "BonvenoCFLight", sans-serif;
|
:root {
|
||||||
@console-font-family: "ConsolaMono", monospace;
|
--header-title-font-family: "BonvenoCFLight", sans-serif;
|
||||||
|
--console-font-family: "ConsolaMono", monospace;
|
||||||
@amber-penguin-yellow: #FFC200;
|
--amber-penguin-yellow: #FFC200;
|
||||||
|
}
|
||||||
|
|
||||||
/*** General containers ***/
|
/*** General containers ***/
|
||||||
.sea-header {
|
.sea-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
color: @amber-penguin-yellow;
|
color: var(--amber-penguin-yellow);
|
||||||
padding: 15px 0;
|
padding: 15px 0;
|
||||||
|
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
margin-right: -30px;
|
margin-right: -30px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
|
||||||
.shadowed_border;
|
/* mixin: .shadowed_border;*/
|
||||||
|
border-radius: 25px;
|
||||||
|
border: 1px solid #111111;
|
||||||
|
box-shadow: 5px 5px #111111;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ice-floe {
|
.ice-floe {
|
||||||
.shadowed_border;
|
/* mixin: .shadowed_border;*/
|
||||||
|
border-radius: 25px;
|
||||||
|
border: 1px solid #111111;
|
||||||
|
box-shadow: 5px 5px #111111;
|
||||||
|
|
||||||
padding: 15px 30px 15px 30px;
|
padding: 15px 30px 15px 30px;
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ice-menu {
|
.ice-menu {
|
||||||
.shadowed_border;
|
/* mixin: .shadowed_border;*/
|
||||||
|
border-radius: 25px;
|
||||||
|
border: 1px solid #111111;
|
||||||
|
box-shadow: 5px 5px #111111;
|
||||||
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
@ -87,11 +98,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
font-family: @header-title-font-family;
|
font-family: var(--header-title-font-family);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sea-header .header_title {
|
.sea-header .header_title {
|
||||||
color: @amber-penguin-yellow;
|
color: var(--amber-penguin-yellow);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
@ -103,7 +114,7 @@ h1, h2 {
|
||||||
|
|
||||||
.sea-header .header_tagline {
|
.sea-header .header_tagline {
|
||||||
font-size: 12pt;
|
font-size: 12pt;
|
||||||
font-family: @console-font-family;
|
font-family: var(--console-font-family);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #eeeeee;
|
color: #eeeeee;
|
||||||
}
|
}
|
||||||
|
@ -112,13 +123,17 @@ h1, h2 {
|
||||||
a,
|
a,
|
||||||
.hoverable {
|
.hoverable {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
.small_bordered;
|
/* mixin: .small_bordered; */
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover,
|
a:hover,
|
||||||
.hoverable:hover {
|
.hoverable:hover {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
.small_bordered;
|
/* mixin: .small_bordered; */
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.navigation-bar {
|
ul.navigation-bar {
|
||||||
|
@ -127,29 +142,33 @@ ul.navigation-bar {
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: @header-title-font-family;
|
font-family: var(--header-title-font-family);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.navigation-bar > li > a {
|
ul.navigation-bar > li > a {
|
||||||
.small_bordered;
|
/* mixin: .small_bordered;*/
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** Navigation overrides. ***/
|
/*** Navigation overrides. ***/
|
||||||
.nav {
|
.nav {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: @header-title-font-family;
|
font-family: var(--header-title-font-family);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav > li {
|
.nav > li {
|
||||||
.small_bordered;
|
/* mixin: .small_bordered; */
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** Footer ***/
|
/*** Footer ***/
|
||||||
footer {
|
footer {
|
||||||
font-family: @console-font-family;
|
font-family: var(--console-font-family);
|
||||||
font-size: small;
|
font-size: small;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -172,9 +191,7 @@ form > ul > li {
|
||||||
|
|
||||||
.shadowed_border {
|
.shadowed_border {
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
border-style: solid;
|
border: 1px solid #111111;
|
||||||
border-width: 1px;
|
|
||||||
border-color: #111111;
|
|
||||||
box-shadow: 5px 5px #111111;
|
box-shadow: 5px 5px #111111;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Penguin Daytime Theme
|
||||||
|
*
|
||||||
|
* @author: Dorian Pula (dorian.pula@amber-penguin-software.ca)
|
||||||
|
* @company: Amber Penguin Software
|
||||||
|
* @note: A soothing light grey and blue CSS theme.
|
||||||
|
* @version: 0.5.0
|
||||||
|
* @license: AGPL v3
|
||||||
|
*/
|
||||||
|
@import url("penguin-common-theme.css");
|
||||||
|
|
||||||
|
/*** Colours. ***/
|
||||||
|
:root {
|
||||||
|
--daytime-body-background-colour: #d0d0d0;
|
||||||
|
--daytime-element-background-colour: #ffffff;
|
||||||
|
|
||||||
|
--daytime-header-title-colour: #2b2a2b;
|
||||||
|
--daytime-header-background-colour: #777777;
|
||||||
|
--daytime-subheader-title-colour: #5b5b5b;
|
||||||
|
|
||||||
|
--daytime-link-highlight-colour: #46a0ff;
|
||||||
|
--daytime-link-highlight-colour-text: #ffffff;
|
||||||
|
--daytime-link-text-colour: #0075b4;
|
||||||
|
|
||||||
|
--daytime-footer-text-colour: #787878;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** General containers ***/
|
||||||
|
body.daytime .sea-header {
|
||||||
|
background-color: var(--daytime-header-background-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.daytime .ice-floe,
|
||||||
|
body.daytime .ice-menu {
|
||||||
|
background-color: var(--daytime-element-background-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.daytime .navigation-bar {
|
||||||
|
background-color: var(--daytime-element-background-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** General elements ***/
|
||||||
|
body.daytime {
|
||||||
|
background-color: var(--daytime-body-background-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Footer ***/
|
||||||
|
body.daytime footer {
|
||||||
|
color: var(--daytime-footer-text-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Article elements. ***/
|
||||||
|
body.daytime h1 {
|
||||||
|
color: var(--daytime-header-title-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.daytime h2 {
|
||||||
|
color: var(--daytime-subheader-title-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Navigation ***/
|
||||||
|
body.daytime a,
|
||||||
|
body.daytime .hoverable {
|
||||||
|
color: var(--daytime-link-text-colour);
|
||||||
|
/* mixin: .small_bordered; */
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.daytime a:hover,
|
||||||
|
body.daytime .hoverable:hover {
|
||||||
|
background-color: var(--daytime-link-highlight-colour);
|
||||||
|
color: var(--daytime-link-highlight-colour-text);
|
||||||
|
|
||||||
|
/* mixin: .small_bordered; */
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hacks to resolve colour scheme issues in Bootstrap. */
|
||||||
|
body.daytime .nav-pills > .active > a,
|
||||||
|
body.daytime .nav-pills > .active > a:hover,
|
||||||
|
body.daytime .nav-pills > .active > a:focus {
|
||||||
|
background-color: var(--daytime-link-highlight-colour);
|
||||||
|
color: var(--daytime-link-highlight-colour-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.daytime .nav > li > a:hover,
|
||||||
|
body.daytime .nav > li > a:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: var(--daytime-link-highlight-colour);
|
||||||
|
color: var(--daytime-link-highlight-colour-text);
|
||||||
|
}
|
|
@ -1,94 +0,0 @@
|
||||||
/*
|
|
||||||
* Penguin Daytime Theme
|
|
||||||
*
|
|
||||||
* @author: Dorian Pula (dorian.pula@amber-penguin-software.ca)
|
|
||||||
* @company: Amber Penguin Software
|
|
||||||
* @note: A soothing light grey and blue CSS theme.
|
|
||||||
* @version: 0.5.0
|
|
||||||
* @license: AGPL v3
|
|
||||||
*/
|
|
||||||
@import (less) "penguin-common-theme.less";
|
|
||||||
|
|
||||||
/*** Colours. ***/
|
|
||||||
@daytime-body-background-colour: #d0d0d0;
|
|
||||||
@daytime-element-background-colour: #ffffff;
|
|
||||||
|
|
||||||
@daytime-header-title-colour: #2b2a2b;
|
|
||||||
@daytime-header-background-colour: #777777;
|
|
||||||
@daytime-subheader-title-colour: #5b5b5b;
|
|
||||||
|
|
||||||
@daytime-link-highlight-colour: #46a0ff;
|
|
||||||
@daytime-link-highlight-colour-text: #ffffff;
|
|
||||||
@daytime-link-text-colour: #0075b4;
|
|
||||||
|
|
||||||
@daytime-footer-text-colour: #787878;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Build out themes based on these ideas:
|
|
||||||
- http://stackoverflow.com/questions/15366576/less-condition-based-on-css-class-to-set-a-less-variable
|
|
||||||
- http://stackoverflow.com/questions/15275829/less-css-change-variable-value-for-theme-colors-depending-on-body-class/
|
|
||||||
15279979#15279979
|
|
||||||
**/
|
|
||||||
@daytime-theme-name: .daytime;
|
|
||||||
|
|
||||||
/*** General containers ***/
|
|
||||||
body@{daytime-theme-name} .sea-header {
|
|
||||||
background-color: @daytime-header-background-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
body@{daytime-theme-name} .ice-floe,
|
|
||||||
body@{daytime-theme-name} .ice-menu {
|
|
||||||
background-color: @daytime-element-background-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
body@{daytime-theme-name} .navigation-bar {
|
|
||||||
background-color: @daytime-element-background-colour;
|
|
||||||
}s
|
|
||||||
|
|
||||||
/*** General elements ***/
|
|
||||||
body@{daytime-theme-name} {
|
|
||||||
background-color: @daytime-body-background-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Footer ***/
|
|
||||||
body@{daytime-theme-name} footer {
|
|
||||||
color: @daytime-footer-text-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Article elements. ***/
|
|
||||||
body@{daytime-theme-name} h1 {
|
|
||||||
color: @daytime-header-title-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
body@{daytime-theme-name} h2 {
|
|
||||||
color: @daytime-subheader-title-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Navigation ***/
|
|
||||||
body@{daytime-theme-name} a,
|
|
||||||
body@{daytime-theme-name} .hoverable {
|
|
||||||
color: @daytime-link-text-colour;
|
|
||||||
.small_bordered;
|
|
||||||
}
|
|
||||||
|
|
||||||
body@{daytime-theme-name} a:hover,
|
|
||||||
body@{daytime-theme-name} .hoverable:hover {
|
|
||||||
background-color: @daytime-link-highlight-colour;
|
|
||||||
color: @daytime-link-highlight-colour-text;
|
|
||||||
.small_bordered;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hacks to resolve colour scheme issues in Bootstrap.
|
|
||||||
body@{daytime-theme-name} .nav-pills > .active > a,
|
|
||||||
body@{daytime-theme-name} .nav-pills > .active > a:hover,
|
|
||||||
body@{daytime-theme-name} .nav-pills > .active > a:focus {
|
|
||||||
background-color: @daytime-link-highlight-colour;
|
|
||||||
color: @daytime-link-highlight-colour-text;
|
|
||||||
}
|
|
||||||
|
|
||||||
body@{daytime-theme-name} .nav > li > a:hover,
|
|
||||||
body@{daytime-theme-name} .nav > li > a:focus {
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: @daytime-link-highlight-colour;
|
|
||||||
color: @daytime-link-highlight-colour-text;
|
|
||||||
}
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* Penguin Evening Theme
|
||||||
|
*
|
||||||
|
* @author: Dorian Pula (dorian.pula@amber-penguin-software.ca)
|
||||||
|
* @company: Amber Penguin Software
|
||||||
|
* @note: A soothing grey and amber CSS theme.
|
||||||
|
* @version: 0.5.0
|
||||||
|
* @license: AGPL v3
|
||||||
|
*/
|
||||||
|
@import url("penguin-common-theme.css");
|
||||||
|
|
||||||
|
/*** Colours. ***/
|
||||||
|
:root {
|
||||||
|
--evening-body-background-colour: #292929;
|
||||||
|
--evening-element-background-colour: #777777;
|
||||||
|
|
||||||
|
--evening-header-title-colour: var(--amber-penguin-yellow);
|
||||||
|
--evening-header-background-colour: #454545;
|
||||||
|
--evening-subheader-title-colour: #CCCCCC;
|
||||||
|
|
||||||
|
--evening-link-highlight-colour: var(--amber-penguin-yellow);
|
||||||
|
--evening-link-highlight-colour-text: var(--evening-body-background-colour);
|
||||||
|
--evening-link-text-colour: var(--amber-penguin-yellow);
|
||||||
|
|
||||||
|
--evening-footer-text-colour: #CCCCCC;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** General containers ***/
|
||||||
|
body.evening .sea-header {
|
||||||
|
background-color: var(--evening-header-background-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.evening .ice-floe,
|
||||||
|
body.evening .ice-menu {
|
||||||
|
background-color: var(--evening-element-background-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.evening .navigation-bar {
|
||||||
|
background-color: var(--evening-element-background-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** General elements ***/
|
||||||
|
body.evening {
|
||||||
|
background-color: var(--evening-body-background-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Footer ***/
|
||||||
|
body.evening footer {
|
||||||
|
color: var(--evening-footer-text-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Article elements. ***/
|
||||||
|
body.evening h1 {
|
||||||
|
color: var(--evening-header-title-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.evening h2 {
|
||||||
|
color: var(--evening-subheader-title-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Navigation ***/
|
||||||
|
body.evening a,
|
||||||
|
body.evening .hoverable {
|
||||||
|
color: var(--evening-link-text-colour);
|
||||||
|
/* mixin: .small_bordered; */
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.evening a:hover,
|
||||||
|
body.evening .hoverable:hover {
|
||||||
|
background-color: var(--evening-link-highlight-colour);
|
||||||
|
color: var(--evening-link-highlight-colour-text);
|
||||||
|
/* mixin: .small_bordered; */
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hacks to resolve colour scheme issues in Bootstrap. */
|
||||||
|
body.evening .nav-pills > .active > a,
|
||||||
|
body.evening .nav-pills > .active > a:hover,
|
||||||
|
body.evening .nav-pills > .active > a:focus {
|
||||||
|
background-color: var(--evening-link-highlight-colour);
|
||||||
|
color: var(--evening-link-highlight-colour-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.evening .nav > li > a:hover,
|
||||||
|
body.evening .nav > li > a:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: var(--evening-link-highlight-colour);
|
||||||
|
color: var(--evening-link-highlight-colour-text);
|
||||||
|
}
|
|
@ -1,94 +0,0 @@
|
||||||
/*
|
|
||||||
* Penguin Evening Theme
|
|
||||||
*
|
|
||||||
* @author: Dorian Pula (dorian.pula@amber-penguin-software.ca)
|
|
||||||
* @company: Amber Penguin Software
|
|
||||||
* @note: A soothing grey and amber CSS theme.
|
|
||||||
* @version: 0.5.0
|
|
||||||
* @license: AGPL v3
|
|
||||||
*/
|
|
||||||
@import (less) "penguin-common-theme.less";
|
|
||||||
|
|
||||||
/*** Colours. ***/
|
|
||||||
@evening-body-background-colour: #292929;
|
|
||||||
@evening-element-background-colour: #777777;
|
|
||||||
|
|
||||||
@evening-header-title-colour: @amber-penguin-yellow;
|
|
||||||
@evening-header-background-colour: #454545;
|
|
||||||
@evening-subheader-title-colour: #CCCCCC;
|
|
||||||
|
|
||||||
@evening-link-highlight-colour: @amber-penguin-yellow;
|
|
||||||
@evening-link-highlight-colour-text: @evening-body-background-colour;
|
|
||||||
@evening-link-text-colour: @amber-penguin-yellow;
|
|
||||||
|
|
||||||
@evening-footer-text-colour: #CCCCCC;
|
|
||||||
|
|
||||||
/**
|
|
||||||
Build out themes based on these ideas:
|
|
||||||
- http://stackoverflow.com/questions/15366576/less-condition-based-on-css-class-to-set-a-less-variable
|
|
||||||
- http://stackoverflow.com/questions/15275829/less-css-change-variable-value-for-theme-colors-depending-on-body-class/
|
|
||||||
15279979#15279979
|
|
||||||
**/
|
|
||||||
@evening-theme-name: .evening;
|
|
||||||
|
|
||||||
/*** General containers ***/
|
|
||||||
body@{evening-theme-name} .sea-header {
|
|
||||||
background-color: @evening-header-background-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
body@{evening-theme-name} .ice-floe,
|
|
||||||
body@{evening-theme-name} .ice-menu {
|
|
||||||
background-color: @evening-element-background-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
body@{evening-theme-name} .navigation-bar {
|
|
||||||
background-color: @evening-element-background-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** General elements ***/
|
|
||||||
body@{evening-theme-name} {
|
|
||||||
background-color: @evening-body-background-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Footer ***/
|
|
||||||
body@{evening-theme-name} footer {
|
|
||||||
color: @evening-footer-text-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Article elements. ***/
|
|
||||||
body@{evening-theme-name} h1 {
|
|
||||||
color: @evening-header-title-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
body@{evening-theme-name} h2 {
|
|
||||||
color: @evening-subheader-title-colour;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Navigation ***/
|
|
||||||
body@{evening-theme-name} a,
|
|
||||||
body@{evening-theme-name} .hoverable {
|
|
||||||
color: @evening-link-text-colour;
|
|
||||||
.small_bordered;
|
|
||||||
}
|
|
||||||
|
|
||||||
body@{evening-theme-name} a:hover,
|
|
||||||
body@{evening-theme-name} .hoverable:hover {
|
|
||||||
background-color: @evening-link-highlight-colour;
|
|
||||||
color: @evening-link-highlight-colour-text;
|
|
||||||
.small_bordered;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hacks to resolve colour scheme issues in Bootstrap.
|
|
||||||
body@{evening-theme-name} .nav-pills > .active > a,
|
|
||||||
body@{evening-theme-name} .nav-pills > .active > a:hover,
|
|
||||||
body@{evening-theme-name} .nav-pills > .active > a:focus {
|
|
||||||
background-color: @evening-link-highlight-colour;
|
|
||||||
color: @evening-link-highlight-colour-text;
|
|
||||||
}
|
|
||||||
|
|
||||||
body@{evening-theme-name} .nav > li > a:hover,
|
|
||||||
body@{evening-theme-name} .nav > li > a:focus {
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: @evening-link-highlight-colour;
|
|
||||||
color: @evening-link-highlight-colour-text;
|
|
||||||
}
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* Rookeries CSS
|
||||||
|
*
|
||||||
|
* @author: Dorian Pula (dorian.pula@amber-penguin-software.ca)
|
||||||
|
* @company: Amber Penguin Software
|
||||||
|
* @version: 0.5.0
|
||||||
|
* @license: AGPL v3
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import url("external-embedded-styles.css");
|
||||||
|
@import url("penguin-common-theme.css");
|
||||||
|
@import url("penguin-daytime-theme.css");
|
||||||
|
@import url("penguin-evening-theme.css");
|
|
@ -1,13 +0,0 @@
|
||||||
/*
|
|
||||||
* Rookeries CSS
|
|
||||||
*
|
|
||||||
* @author: Dorian Pula (dorian.pula@amber-penguin-software.ca)
|
|
||||||
* @company: Amber Penguin Software
|
|
||||||
* @version: 0.5.0
|
|
||||||
* @license: AGPL v3
|
|
||||||
*/
|
|
||||||
|
|
||||||
@import (less) "penguin-common-theme.less";
|
|
||||||
@import (less) "penguin-daytime-theme.less";
|
|
||||||
@import (less) "penguin-evening-theme.less";
|
|
||||||
@import (less) "external-embedded-styles.less";
|
|
Binary file not shown.
|
@ -26,21 +26,35 @@ def config(ctx):
|
||||||
|
|
||||||
@inv.task
|
@inv.task
|
||||||
def prepare_assets(ctx):
|
def prepare_assets(ctx):
|
||||||
inv.run('npm run build', echo=True, pty=True)
|
ctx.run('npm run build', echo=True, pty=True)
|
||||||
|
|
||||||
|
|
||||||
|
@inv.task
|
||||||
|
def run_webpack_server(ctx):
|
||||||
|
ctx.run('npm run dev-server', echo=True, pty=True)
|
||||||
|
|
||||||
|
|
||||||
@inv.task
|
@inv.task
|
||||||
def wait(ctx, timeout=10):
|
def wait(ctx, timeout=10):
|
||||||
inv.run(f'dockerize -wait http://rookeries:5000/status -timeout {timeout}s')
|
ctx.run(f'dockerize -wait http://rookeries:5000/status -timeout {timeout}s')
|
||||||
|
|
||||||
|
|
||||||
@inv.task(prepare_assets)
|
@inv.task(prepare_assets)
|
||||||
def run(ctx, port=5000):
|
def run(ctx, port=5000):
|
||||||
"""
|
"""
|
||||||
Run the API app in a server.
|
Run the API app in a server
|
||||||
By default this runs a production uWSGI server.
|
By default this runs a production uWSGI server.
|
||||||
|
|
||||||
:param ctx: Context of the invoke task.
|
:param ctx: Context of the invoke task.
|
||||||
:param port: The port to run the API app on. 5000 by default.
|
:param port: The port to run the API app on. 5000 by default.
|
||||||
"""
|
"""
|
||||||
inv.run(f'uwsgi --http :{port} --master --processes 4 --module "rookeries:make_rookeries_app()"')
|
|
||||||
|
uwsgi_cmd = ' '.join([
|
||||||
|
'uwsgi',
|
||||||
|
f'--http :{port}',
|
||||||
|
'--master',
|
||||||
|
'--processes 4',
|
||||||
|
'--module "rookeries:make_rookeries_app()"',
|
||||||
|
])
|
||||||
|
|
||||||
|
ctx.run(uwsgi_cmd, echo=True, pty=True)
|
||||||
|
|
|
@ -64,14 +64,12 @@ def js_style(ctx):
|
||||||
|
|
||||||
:param ctx: Context of the invoke task.
|
:param ctx: Context of the invoke task.
|
||||||
"""
|
"""
|
||||||
run_eslint = 'node node_modules/eslint/bin/eslint.js --ignore-pattern "/dist/*" --ignore-pattern "/stores/*" src'
|
inv.run('npm run lint', echo=True, pty=True)
|
||||||
inv.run(run_eslint, echo=True, pty=True)
|
|
||||||
|
|
||||||
|
|
||||||
@inv.task
|
@inv.task
|
||||||
def js(ctx):
|
def js(ctx):
|
||||||
run_mocha = 'BABEL_DISABLE_CACHE=yes node_modules/mocha/bin/mocha --require tests/babelhook tests/unit'
|
inv.run('npm run test', echo=True, pty=True)
|
||||||
inv.run(run_mocha, echo=True, pty=True)
|
|
||||||
|
|
||||||
|
|
||||||
@inv.task
|
@inv.task
|
||||||
|
|
|
@ -5,14 +5,13 @@
|
||||||
<link rel="icon" type="image/icon" href="{{ site['favicon'] }}"/>
|
<link rel="icon" type="image/icon" href="{{ site['favicon'] }}"/>
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/static/css/font-awesome.css"/>
|
<link rel="stylesheet" type="text/css" media="screen" href="/static/js/rookeries.css"/>
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/static/css/rookeries-bundle.css"/>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="evening">
|
<body class="evening">
|
||||||
<div id="ui-target">
|
<div id="ui-target">
|
||||||
{{ react_render | safe }}
|
{{ react_render | safe }}
|
||||||
</div>
|
</div>
|
||||||
<script type="application/javascript" src="/static/js/rookeries-client.js"></script>
|
<script type="application/javascript" src="/static/js/rookeries.js"></script>
|
||||||
<input type="hidden" name="rendered_path" value="{{ page['path'] }}"/>
|
<input type="hidden" name="rendered_path" value="{{ page['path'] }}"/>
|
||||||
<input type="hidden" name="site_name" value="{{ site['name'] }}"/>
|
<input type="hidden" name="site_name" value="{{ site['name'] }}"/>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
|
||||||
|
<link rel="stylesheet" type="text/css" media="screen" href="/static/js/rookeries.css"/>
|
||||||
|
</head>
|
||||||
|
<body class="evening">
|
||||||
|
<div id="ui-target"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,4 +1,4 @@
|
||||||
require("babel-register")({
|
require("babel-register")({
|
||||||
presets: ["es2015", "react", "stage-0"],
|
presets: ["es2017", "es2015", "react", "stage-0"],
|
||||||
plugins: ["babel-plugin-rewire"]
|
plugins: ["babel-plugin-rewire"]
|
||||||
});
|
});
|
|
@ -0,0 +1,41 @@
|
||||||
|
/* global describe it beforeEach afterEach */
|
||||||
|
|
||||||
|
import {h} from "preact"; /** @jsx h */
|
||||||
|
|
||||||
|
import { JournalMarkdownView } from "../../src/views/journal_markdown_viewer";
|
||||||
|
|
||||||
|
import chai, {assert, expect} from "chai";
|
||||||
|
import assertJsx from "preact-jsx-chai";
|
||||||
|
|
||||||
|
chai.use(assertJsx);
|
||||||
|
|
||||||
|
describe('JournalMarkdownViewer', () => {
|
||||||
|
|
||||||
|
describe("simple test", () => {
|
||||||
|
it("prove that test infrastructure works", () => {
|
||||||
|
assert.ok(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Fetching of Articles by view linked to store", () => {
|
||||||
|
it('The view should default to a loading text when initializing and then calling for data', () => {
|
||||||
|
expect(<JournalMarkdownView />)
|
||||||
|
.to.not.eql(
|
||||||
|
<div>
|
||||||
|
<h1>Loading...</h1>
|
||||||
|
<span>Please wait while we load</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(<JournalMarkdownView />)
|
||||||
|
.to.contain(
|
||||||
|
<div>
|
||||||
|
<h1 />
|
||||||
|
<span dangerouslySetInnerHTML={{"__html": "<p>Please wait while we load this page's entry.</p>\n"}} />
|
||||||
|
<div />
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,16 +7,16 @@ Feature: Site Access
|
||||||
Scenario: Any user can get an existing site
|
Scenario: Any user can get an existing site
|
||||||
Given I am a visitor
|
Given I am a visitor
|
||||||
And I get the site
|
And I get the site
|
||||||
Then I get a valid site
|
# Then I get a valid site
|
||||||
# And I get links to a menu
|
# And I get links to a menu
|
||||||
# And I get links to a landing page
|
# And I get links to a landing page
|
||||||
|
|
||||||
Scenario: An admin can get an existing site
|
#Scenario: An admin can get an existing site
|
||||||
Given I am an authenticated admin user
|
# Given I am an authenticated admin user
|
||||||
And I get the site
|
# And I get the site
|
||||||
Then I get a valid site
|
# Then I get a valid site
|
||||||
|
|
||||||
Scenario: An editor can get an existing site
|
#Scenario: An editor can get an existing site
|
||||||
Given I am an authenticated editor user
|
# Given I am an authenticated editor user
|
||||||
And I get the site
|
# And I get the site
|
||||||
Then I get a valid site
|
# Then I get a valid site
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
// From https://github.com/jesstelford/react-testing-mocha-jsdom
|
|
||||||
import jsdom from 'jsdom';
|
|
||||||
|
|
||||||
// A super simple DOM ready for React to render into
|
|
||||||
// Store this DOM and the window in global scope ready for React to access
|
|
||||||
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
|
|
||||||
global.window = document.parentWindow;
|
|
||||||
|
|
||||||
// TODO: Insert rest of imports here... after creating the global window.
|
|
||||||
import React from "react";
|
|
||||||
import TestUtils from "react-addons-test-utils";
|
|
||||||
import sinon from "sinon";
|
|
||||||
import { assert } from "chai";
|
|
||||||
|
|
||||||
import { Actions } from "../../src/actions.js";
|
|
||||||
// import { JournalEntryStore } from "../src/stores/journal_entry_store.js";
|
|
||||||
import { JournalMarkdownView, __RewireAPI__ as JournalRewireAPI } from "../../src/views/journal_markdown_viewer.js";
|
|
||||||
import { Router, State } from "react-router";
|
|
||||||
|
|
||||||
describe('JournalMarkdownViewer', function() {
|
|
||||||
describe("simple test", function () {
|
|
||||||
it("prove that test infrastructure works", function () {
|
|
||||||
assert.ok(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.skip("Fetching of Articles by view linked to store", function () {
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
JournalRewireAPI.__Rewire__("Actions", sinon.stub(Actions));
|
|
||||||
// JournalRewireAPI.__Rewire__("JournalEntryStore", sinon.stub(JournalEntryStore));
|
|
||||||
JournalRewireAPI.__Rewire__("State", sinon.stub(State));
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
console.log('After');
|
|
||||||
JournalRewireAPI.__ResetDependency__("Actions");
|
|
||||||
JournalRewireAPI.__ResetDependency__("JournalEntryStore");
|
|
||||||
JournalRewireAPI.__ResetDependency__("State");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('The view should default to a loading text when initializing and then calling for data', function () {
|
|
||||||
this.skip('Fix up later');
|
|
||||||
let journalMarkdownView = TestUtils.renderIntoDocument(<JournalMarkdownView />);
|
|
||||||
|
|
||||||
assert.ok(journalMarkdownView);
|
|
||||||
|
|
||||||
let journalTitle = TestUtils.findRenderedDOMComponentWithTag(journalMarkdownView, 'h1');
|
|
||||||
assert.ok(journalTitle);
|
|
||||||
assert.equal(journalTitle.getDOMNode().textContent, 'Loading...');
|
|
||||||
|
|
||||||
let journalContent = TestUtils.findRenderedDOMComponentWithTag(journalMarkdownView, 'span');
|
|
||||||
assert.ok(journalContent);
|
|
||||||
assert.include(journalContent.getDOMNode().textContent, 'Please wait while we load');
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO Add timer test
|
|
||||||
// TODO Add mock call for data.
|
|
||||||
|
|
||||||
it('The view should update its state given newer data', function () {
|
|
||||||
this.skip('Implement me.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
const path = require('path');
|
||||||
|
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
// TODO: Enable with production configuration.
|
||||||
|
// const MinifyPlugin = require("babel-minify-webpack-plugin");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: ['babel-polyfill', './src/entry.js'],
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, 'static', 'js'),
|
||||||
|
filename: 'rookeries.js'
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
enforce: "pre",
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: [
|
||||||
|
/node_modules/,
|
||||||
|
],
|
||||||
|
loader: "eslint-loader",
|
||||||
|
options: {
|
||||||
|
emitWarning: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.js/,
|
||||||
|
include: [
|
||||||
|
path.resolve(__dirname, "src"),
|
||||||
|
],
|
||||||
|
exclude: [
|
||||||
|
/node_modules/,
|
||||||
|
],
|
||||||
|
|
||||||
|
loader: 'babel-loader'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ExtractTextPlugin.extract({
|
||||||
|
fallback: 'style-loader',
|
||||||
|
use: 'css-loader'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.html/,
|
||||||
|
loader: 'html-loader',
|
||||||
|
},
|
||||||
|
|
||||||
|
// From https://github.com/shakacode/font-awesome-loader/blob/master/docs/usage-webpack2.md
|
||||||
|
{
|
||||||
|
test: /\.woff2?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||||
|
// Limiting the size of the woff fonts breaks font-awesome ONLY for the extract text plugin
|
||||||
|
// loader: "url?limit=10000"
|
||||||
|
use: "url-loader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(ttf|eot|svg)(\?[\s\S]+)?$/,
|
||||||
|
use: 'file-loader'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
devtool: 'source-map',
|
||||||
|
|
||||||
|
devServer: {
|
||||||
|
contentBase: './',
|
||||||
|
host: "0.0.0.0",
|
||||||
|
hot: true,
|
||||||
|
port: 3000,
|
||||||
|
proxy: {
|
||||||
|
"/api": "http://localhost:5000",
|
||||||
|
"/auth": "http://localhost:5000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'react': 'preact-compat',
|
||||||
|
'react-dom': 'preact-compat',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new ExtractTextPlugin('rookeries.css'),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
favicon: './static/images/mr-penguin-amber-favicon.ico',
|
||||||
|
title: 'Rookeries - Webpack Dev',
|
||||||
|
template: './templates/webpack_base.html',
|
||||||
|
xhtml: true
|
||||||
|
}),
|
||||||
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
// TODO: Enable in the future.
|
||||||
|
// new MinifyPlugin(),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.BROWSER_ENV': JSON.stringify(true)
|
||||||
|
})
|
||||||
|
]
|
||||||
|
};
|
Loading…
Reference in New Issue