Merged in frontend-modernization (pull request #27)

Frontend modernization

Approved-by: Dorian Pula <dorian.pula@amber-penguin-software.ca>
This commit is contained in:
Dorian 2017-09-24 19:11:04 +00:00
commit c8decceb8b
59 changed files with 6129 additions and 4537 deletions

32
.babelrc Normal file
View File

@ -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
}

View File

@ -9,10 +9,36 @@ jobs:
- checkout
- setup_docker_engine
- run:
name: Build Docker images
command: make build
# Enable caching: https://circleci.com/blog/how-to-build-a-docker-image-on-circleci-2-0/
- restore_cache:
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:
name: Test API
command: make test-api

View File

@ -6,11 +6,16 @@
"react",
"import"
],
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 7,
"ecmaFeatures": {
"modules": true,
"jsx": true
}
},
"parser": "babel-eslint",
"rules": {
"arrow-body-style": [2, "always"],
"arrow-parens": 2,
@ -21,6 +26,7 @@
"default-case": 1,
"dot-notation": 2,
"eqeqeq": 2,
"indent": [1, 2],
"jsx-quotes": 1,
"no-console": 0,
"no-alert": 2,
@ -54,14 +60,14 @@
"react/forbid-prop-types": 1,
"react/jsx-boolean-value": 1,
"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-handler-names": 1,
"react/jsx-indent-props": 1,
"react/jsx-indent": 1,
"react/jsx-indent-props": [1, 2],
"react/jsx-indent": [1, 2],
"react/jsx-key": 1,
"react/jsx-max-props-per-line": 0,
"react/jsx-wrap-multilines": 1,
"react/jsx-no-bind": 1,
"react/jsx-no-duplicate-props": 1,
"react/jsx-no-literals": 0,
@ -70,6 +76,7 @@
"react/jsx-sort-props": 0,
"react/jsx-uses-react": 1,
"react/jsx-uses-vars": 1,
"react/jsx-wrap-multilines": 1,
"react/no-danger": 0,
"react/no-deprecated": 1,
"react/no-did-mount-set-state": 1,
@ -87,6 +94,7 @@
"react/sort-comp": 1,
"react/sort-prop-types": 1
},
"env": {
"browser": true,
"node": true,

View File

@ -1,9 +1,13 @@
FROM python:3.6
FROM python:3.6-slim
MAINTAINER Dorian Pula <dorian.pula@amber-penguin-software.ca>
# 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 \
&& dpkg -i dumb-init_*.deb
&& dpkg -i dumb-init_*.deb \
&& rm dumb-init_*.deb
ENV DOCKERIZE_VERSION v0.3.0
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
# Install Node and Yarn
RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - \
&& apt-get install -y nodejs \
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - \
&& 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 \
&& apt-get update \
&& apt-get install -y yarn
&& apt-get install -y nodejs yarn
# Install Python dependencies
COPY requirements.txt /app/rookeries/
RUN pip install --requirement /app/rookeries/requirements.txt
# Install Node dependencies
COPY package.json /app/rookeries/
COPY yarn.lock /app/rookeries/
RUN cd /app/rookeries && yarn install
COPY ["package.json", "yarn.lock", "/app/rookeries/"]
RUN cd /app/rookeries && yarn install --silent
# Copy in Rookeries server code
COPY . /app/rookeries/

View File

@ -6,29 +6,37 @@ build-git-tag:
# Build the API image
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
test: build test-api test-ui
# Runs API tests
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 exec rookeries inv db.wait
docker-compose exec rookeries inv db.init
docker-compose exec rookeries inv db.upgrade
docker-compose exec rookeries inv server.wait --timeout 30
docker-compose exec rookeries inv test.server
docker-compose exec rookeries \
inv db.wait db.init db.upgrade
docker-compose exec rookeries \
inv server.wait --timeout 100
docker-compose exec rookeries \
inv test.server
# Runs UI tests
test-ui: stop build
docker-compose run --no-deps rookeries inv test.js_style
docker-compose run --no-deps rookeries inv test.js
docker-compose run --no-deps rookeries \
inv test.js_style test.js
docker-compose up -d
docker-compose exec rookeries inv db.wait
docker-compose exec rookeries inv server.wait --timeout 100
docker-compose exec rookeries inv test.ui
docker-compose exec rookeries \
inv db.wait
docker-compose exec rookeries \
inv server.wait --timeout 100
docker-compose exec rookeries \
inv test.ui
# Demos Rookeries in a browser
demo: build

37
Pipfile Normal file
View File

@ -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"

505
Pipfile.lock generated Normal file
View File

@ -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": {}
}

View File

@ -4,8 +4,6 @@ services:
rookeries:
build: .
image: dorianpula/rookeries:latest
ports:
- "5000:5000"
environment:
- ROOKERIES_SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://admin:password@db:5432/rookeries
- ROOKERIES_JWT_SECRET_KEY=problematic_penguins
@ -20,8 +18,6 @@ services:
db:
image: postgres:9.6
ports:
- "5432:5432"
environment:
- POSTGRES_USER=admin
- POSTGRES_PASSWORD=password

View File

@ -5,7 +5,7 @@
- Sports a flexible layout engine, and intuitive editor.
- Supports multiple themes and user personalization.
- 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.
## Powered By
@ -24,5 +24,5 @@ below technologies (and many more). Thank you!!!
| 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 /> | ![Less](http://lesscss.org/public/img/logo.png) <img alt="Bootstrap" src="http://getbootstrap.com/assets/brand/bootstrap-solid.svg" width=100 />| ![CodeMirror](http://codemirror.net/doc/logo.png) |
| [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/)
| <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 />| ![CodeMirror](http://codemirror.net/doc/logo.png) |
| [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/)

16
docs/devnotes.md Normal file
View File

@ -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.

View File

@ -1,59 +1,9 @@
{
"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.",
"engines": {
"node": ">=6.0.0 <7.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"
"node": ">=8.0.0 <9.0.0"
},
"repository": {
"type": "git",
@ -65,18 +15,81 @@
"url": "https://bitbucket.org/dorianpula/rookeries/issues"
},
"browser": "./src/index.js",
"babel": {
"presets": [
"es2015",
"react",
"stage-0"
],
"sourceMaps": true
"dependencies": {
"babel-polyfill": "^6.5.0",
"bootstrap": "4.0.0-beta",
"codemirror": "^5.30.0",
"es6-promise": "^4.1.1",
"font-awesome": "4.7.0",
"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": {
"build": "npm run build-client && npm run build-css && npm run build-server",
"build-client": "browserify src/index.js --outfile static/js/rookeries-client.js --transform [ babelify ]",
"build-css": "lessc static/css/rookeries-css.less static/css/rookeries-bundle.css",
"build-server": "babel src --out-dir dist --source-maps"
"build-client": "webpack",
"build-server": "babel src -d dist",
"build": "npm run build-client && npm run build-server",
"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"
}
}

View File

@ -5,7 +5,7 @@ A developer and designer friendly web platform for building gorgeous sites, blog
**Rookeries** is:
- 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.
## Build Status:
@ -26,11 +26,10 @@ In the meantime, please refer to the development guide below.
*Rookeries* uses the following technologies:
- Python 2.7
- NodeJS 6.9 + ES6
- Python 3.6
- NodeJS 8 + ES2017
- PostgreSQL 9.6
- Docker + docker-compose
- Make
### Getting Started

View File

@ -15,7 +15,7 @@ db = flask_sqlalchemy.SQLAlchemy()
migrations = flask_alembic.Alembic()
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():

View File

@ -17,8 +17,9 @@ from rookeries.vendors import flask_jwt
logger = logging.getLogger(__name__)
@rookeries_app.route('/api/pages/', 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.

View File

@ -1,7 +1,8 @@
"""
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+
"""
@ -24,7 +25,8 @@ logger = logging.getLogger(__name__)
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'] = {
'self': flask.url_for('rookeries_app.get_site', _external=True),
}
@ -40,7 +42,8 @@ def update_site():
flask.abort(http.HTTPStatus.BAD_REQUEST)
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.
current_user = flask_jwt.current_identity
@ -50,7 +53,8 @@ def update_site():
flask.abort(http.HTTPStatus.UNAUTHORIZED)
# 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)
existing_site.base_url = updated_site.base_url
db.session.commit()
@ -61,6 +65,20 @@ def update_site():
@rookeries_app.route('/api/site', methods=['GET'])
def get_site():
site = models.Site.query.filter_by(name=models.MAIN_SITE_ROOT_BLOCK).first_or_404()
site_response = add_self_link(site.to_json())
# site = models.Site.query.filter_by(name=models.MAIN_SITE_ROOT_BLOCK)\
# .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)

View File

@ -95,7 +95,7 @@ def render_single_page_app(some_path=''):
page_info.update(extract_json_response(page_response))
# 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
site_info['name'] = 'Rookeries'

View File

@ -34,27 +34,13 @@ export function initializeApp(applicationStore) {
}
export function fetchPage(pageSlug='') {
return (dispatch) => {
return async (dispatch) => {
let pageUrl = `/api/pages/${pageSlug}`;
if (pageSlug === '') {
console.info(`Loading landing page.`);
// TODO: Retrieve based on the site's default page
} else {
console.info(`Loading... ${pageSlug}`);
}
return fetch(pageUrl)
.then((response) => {
const response = await fetch(`/api/pages/${pageSlug}`);
// TODO: Add better error handling.
// if (response.ok) {
// return response.json();
// } else {
// console.error(`Error getting document JSON - ${res.status} - ${res.text} - Actual ${err}`);
// }
return response.json();
})
.then((json) => {
const json = await response.json();
// TODO: Figure out nicer article/block API.
let articleContent = Reflect.get(json, 'content');
let title = Reflect.get(json, 'title');
@ -65,55 +51,41 @@ export function fetchPage(pageSlug='') {
dispatch(fetchPage("error"));
}
}
return dispatch(loadPage({id: pageSlug, content: articleContent, title: title}));
});
return dispatch(loadPage({slug: pageSlug, content: articleContent, title: title}));
}
}
export function fetchSiteMenu() {
// TODO: Retrieve the name of the site.
return (dispatch) => {
return fetch(`/api/site/menu`)
.then((response) => {
return async (dispatch) => {
const response = await fetch('/api/site/menu');
// TODO: Add better error handling.
// if (response.ok) {
// return response.json();
// } else {
// console.error(`Error getting document JSON - ${res.status} - ${res.text} - Actual ${err}`);
// }
return response.json();
})
.then((json) => {
const json = await response.json();
return dispatch(loadNavigation(json.menu));
});
}
}
export function fetchSiteInfo() {
// TODO: Retrieve the name of the site.
return (dispatch) => {
return fetch("/api/site")
.then((response) => {
return async (dispatch) => {
const response = await fetch("/api/site");
// TODO: Add better error handling.
// if (response.ok) {
// return response.json();
// } else {
// console.error(`Error getting document JSON - ${res.status} - ${res.text} - Actual ${err}`);
// }
const json = response.json();
return response.json();
})
.then((json) => {
// TODO: Avoid this extra conversion
let appSiteInfo = {
const appSiteInfo = {
footer: json.config.footer,
logo: json.config.logo,
name: json.config.name,
tagline: json.config.tagline
};
return dispatch(updateSiteInfo(appSiteInfo));
});
}
}
@ -124,23 +96,21 @@ export function updateSiteInfo(siteInfo) {
};
}
// TODO: Implement this better.
export function savePage(content) {
let pageSlug = content.slug;
return async (dispatch) => {
return (dispatch) => {
let pageUrl = `/api/pages/${pageSlug}`;
return fetch(pageUrl, {
const response = await fetch(`/api/pages/${content.id}`, {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Authorization': `JWT ${UserInfo.getUserInfo('auth_token')}`
},
body: JSON.stringify(content)
}).then((response) => {
return response.json();
}).then((json) => {
});
const json = await response.json();
// TODO: Figure out nicer article/block API.
let articleContent = Reflect.get(json, 'content');
let title = Reflect.get(json, 'title');
@ -153,7 +123,6 @@ export function savePage(content) {
}
}
return dispatch(loadPage({id: articleId, content: articleContent, title: title}));
});
}
}
@ -207,7 +176,7 @@ export function updateUser(token="", user=DEFAULT_USER_LOGIN, accountState="") {
if (token !== "") {
UserInfo.setUserInfo("auth_token", token);
}
UserInfo.setUserInfo("full_name", user.fullName);
UserInfo.setUserInfo("full_name", user.profile.fullName);
return {
type: UPDATE_USER_ACTION,
user: {
@ -233,29 +202,38 @@ export function logoutUser(accountState="") {
export function loginUser(username, password) {
return (dispatch) => {
return fetch("/auth", {
return async (dispatch) => {
const response = await fetch("/auth", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({username: username, password: password})
}).then((response) => {
// TODO: Turn this into a better action:
});
if (response.status !== 200 && response.status !== 401) {
// TODO: Deal with resolving the promise first.
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");
const json = await response.json();
let authToken = Reflect.get(json, "access_token");
if (authToken !== undefined) {
let user = Reflect.get(json, "user");
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}".`));
});
}
}

View File

@ -6,8 +6,12 @@ Rookeries client app
@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 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 {ConnectedThemeSwitchView} from "../views/theme_switcher_button";
@ -15,56 +19,55 @@ import {ConnectedThemeSwitchView} from "../views/theme_switcher_button";
import {SiteHeader} from "../views/site_header";
import {SiteFooter} from "../views/site_footer";
import {ConnectedNavigationMenu} from "../views/navigation_menu";
import {ConnectedArticle} from "../views/journal_markdown_viewer";
// TODO: Add in React Bootstrap rows + columns.
class App extends React.Component {
class App extends Component {
static get propTypes() {
return {
footer: React.PropTypes.string,
siteName: React.PropTypes.string,
logo: React.PropTypes.string,
tagline: React.PropTypes.string
footer: PropTypes.string,
siteName: PropTypes.string,
logo: PropTypes.string,
tagline: PropTypes.string
}
}
render() {
return (
<div>
<Container>
{/*TODO Extract as header container.*/}
<div className="row">
<div className="col-lg-offset-1 col-lg-3">
<Row className="justify-content-around">
<Col lg="4">
<ConnectedUserLogin />
</div>
<div className="col-lg-offset-5 col-lg-2">
</Col>
<Col lg="4">
<ConnectedThemeSwitchView />
</div>
</div>
</Col>
</Row>
{/*TODO Extract as site header container.*/}
<div className="row">
<SiteHeader siteName={ this.props.siteName } logo={ this.props.logo } tagline={ this.props.tagline } />
</div>
{/*TODO Extract as Main body and navigation sidebar containers.*/}
<div className="row">
<div><ConnectedNavigationMenu /></div>
<div className="col-lg-8 ice-floe">
<div>
{ this.props.children }
</div>
</div>
</div>
<Row className="justify-content-center">
<ConnectedNavigationMenu />
<Col lg="8" className="ice-floe">
<Switch>
<Route exact path="/" component={ ConnectedArticle } />
<Route path="/:pageId" component={ ConnectedArticle } />
</Switch>
</Col>
</Row>
{/*TODO Extract as site footer container.*/}
<div className="row">
<SiteFooter footer={ this.props.footer } />
</div>
</div>
</Container>
);
}
}
export const WiredApp = connect(
export const WiredApp = withRouter(connect(
(state) => {
return {
'footer': state.site.footer,
@ -72,4 +75,4 @@ export const WiredApp = connect(
'logo': state.site.logo,
'tagline': state.site.tagline
};
})(App);
})(App));

34
src/entry.js Normal file
View File

@ -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"));

View File

@ -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);
}

View File

@ -7,7 +7,6 @@
*/
import {combineReducers} from 'redux';
import {routerReducer} from 'react-router-redux';
import * as actions from './actions';
// TODO Improve setup of reducers.
@ -100,7 +99,6 @@ const reducer = combineReducers({
site: siteInfoStateReducer,
user: userLoginStateReducer,
theme: themeStateReducer,
routing: routerReducer
});
export {reducer};

View File

@ -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"));
}

View File

@ -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 };

View File

@ -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;
}

38
src/server.js Normal file
View File

@ -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);

View File

@ -7,7 +7,7 @@
*/
import {createStore, applyMiddleware, compose} from "redux";
import createLogger from "redux-logger";
import {createLogger} from "redux-logger";
import thunkMiddleware from "redux-thunk";
import {reducer} from './reducers';

View File

@ -7,7 +7,8 @@ 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;
// TODO: Change out mechanism to use other persistance setup.
const FileSystem = require("fs");
@ -21,6 +22,7 @@ if (typeof localStorage === "undefined" || localStorage === null) {
FileSystem.mkdirSync(tempDirPath);
}
global.localStorage = new LocalStorage(`${tempDirPath}/scratch-${UUID.v4()}`);
}
}

346
src/vendor/Modal.js vendored Normal file
View File

@ -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;

View File

@ -7,14 +7,16 @@
*/
import React from "react";
import {h, Component} from "preact"; /** @jsx h*/
import PropTypes from "prop-types";
/*
Remember to install codemirror before react-codemirror,
see issue https://github.com/JedWatson/react-codemirror/issues/34
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.
// Workaround for rendering CodeMirror server-side
@ -23,12 +25,12 @@ if (typeof(navigator) !== 'undefined') {
}
class CodeEditor extends React.Component {
class CodeEditor extends Component {
static get propTypes() {
return {
code: React.PropTypes.string,
language: React.PropTypes.string,
theme: React.PropTypes.string,
code: PropTypes.string,
language: PropTypes.string,
theme: PropTypes.string,
}
}

View File

@ -7,23 +7,26 @@ Markdown controller to view and update journal entries.
*/
import React from "react";
import {connect} from "react-redux";
import {h, Component} from "preact"; /** @jsx h*/
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 {Button, Panel} from "react-bootstrap";
import {Button, Collapse} from "reactstrap";
import FontAwesome from "react-fontawesome";
import {fetchPage, savePage} from '../actions';
import {appStore} from "../stores";
import {withRouter} from 'react-router-dom';
/*
Remember to install codemirror before react-codemirror,
see issue https://github.com/JedWatson/react-codemirror/issues/34
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.
// Workaround for rendering CodeMirror server-side
@ -40,25 +43,24 @@ export const EditorPaneState = {
};
export class JournalMarkdownView extends React.Component {
export class JournalMarkdownView extends Component {
static get propTypes() {
return {
id: React.PropTypes.string,
article: React.PropTypes.string,
title: React.PropTypes.string,
editorTheme: React.PropTypes.string,
params: React.PropTypes.shape({
pageId: React.PropTypes.string
}),
userCanEdit: React.PropTypes.bool
id: PropTypes.string,
article: PropTypes.string,
title: PropTypes.string,
editorTheme: PropTypes.string,
location: PropTypes.object.isRequired,
match: PropTypes.object,
userCanEdit: PropTypes.bool
}
}
constructor(props) {
super(props);
this.state = {
displayContent: this.props.article || "Please wait while we load this pages entry.",
originalContent: this.props.article || "Please wait while we load this pages entry.",
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);
@ -69,17 +71,24 @@ export class JournalMarkdownView extends React.Component {
}
componentDidMount() {
let pageToLoad = this.props.params.pageId;
let pageToLoad = this.props.match.params.pageId;
appStore.dispatch(fetchPage(pageToLoad));
this.updateEditAllowance(this.props.userCanEdit);
}
componentWillReceiveProps(nextProps) {
this.setState({displayContent: nextProps.article, originalContent: nextProps.article});
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(newCode) {
handleUpdateCode(editor, metadata, newCode) {
this.setState({displayContent: newCode});
}
@ -127,9 +136,13 @@ export class JournalMarkdownView extends React.Component {
render() {
let code = this.state.displayContent;
let rawMarkup = marked(code, {sanitized: true, highlight: (code) => {
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 = (
@ -138,19 +151,22 @@ export class JournalMarkdownView extends React.Component {
</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>
<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 />);
@ -159,7 +175,7 @@ export class JournalMarkdownView extends React.Component {
return (
<div>
<h1>{ this.props.title }</h1>
<span dangerouslySetInnerHTML={ {__html: rawMarkup} }/>
<span dangerouslySetInnerHTML={{ __html: rawMarkup }} />
{ showEditorButton }
{ showPanel }
</div>
@ -167,7 +183,8 @@ export class JournalMarkdownView extends React.Component {
}
}
export const ConnectedArticle = connect(
export const ConnectedArticle = withRouter(
connect(
(state) => {
return {
'id': state.page.id,
@ -176,4 +193,5 @@ export const ConnectedArticle = connect(
'editorTheme': state.editor.theme,
'userCanEdit': state.user.token !== ""
};
})(JournalMarkdownView);
})(JournalMarkdownView)
);

View File

@ -7,32 +7,40 @@ Navigation Menu
*/
import React from "react";
import {Link} from "react-router";
import {h, Component} from "preact"; /** @jsx h*/
import {Link} from "react-router-dom";
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() {
return {
title: React.PropTypes.string,
url: React.PropTypes.string
title: PropTypes.string,
url: PropTypes.string
};
}
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() {
return {
menu: React.PropTypes.arrayOf(
React.PropTypes.shape({
title: React.PropTypes.string,
url: React.PropTypes.url
menu: PropTypes.arrayOf(
PropTypes.shape({
title: PropTypes.string,
url: PropTypes.url
})
)
};
@ -50,11 +58,11 @@ class NavigationMenu extends React.Component {
}
return (
<div className="col-lg-2 col-lg-offset-1 ice-menu">
<nav>
<ul className="nav nav-stacked nav-pills">{ navigationMenuItems }</ul>
</nav>
</div>
<Col lg="3">
<Nav pills vertical className="ice-menu">
{ navigationMenuItems }
</Nav>
</Col>
);
}
}

View File

@ -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() {
return {
footer: React.PropTypes.string
footer: PropTypes.string
}
}
render() {
return (
<div className="row">
<div className="col-lg-6 col-lg-offset-3">
<Row className="justify-content-end">
<Col lg="8">
<footer>{ this.props.footer }</footer>
</div>
</div>
</Col>
</Row>
);
}
}

View File

@ -6,35 +6,38 @@ Site header
@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() {
return {
siteName: React.PropTypes.string,
logo: React.PropTypes.string,
tagline: React.PropTypes.string
siteName: PropTypes.string,
logo: PropTypes.string,
tagline: PropTypes.string
}
}
render() {
let headerLogoAltText = `${this.props.siteName} logo`;
return (
<div className="row">
<div className="col-lg-10 col-lg-offset-1">
<div className="row sea-header">
<div className="col-lg-2 col-lg-offset-3">
<Row className="justify-content-around">
<Col lg="11">
<Row className="sea-header justify-content-center align-items-center">
<Col lg="2">
<img className="header_logo" src={ this.props.logo } alt={ headerLogoAltText }/>
</div>
<div className="col-lg-4">
</Col>
<Col lg="4">
<h1 className="header_title">{ this.props.siteName }</h1>
<h2 className="header_tagline">{ this.props.tagline }</h2>
</div>
</div>
</div>
</div>
</Col>
</Row>
</Col>
</Row>
);
}
}

View File

@ -6,19 +6,21 @@ Switches between premade themes for the site.
@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 {connect} from "react-redux";
import PropTypes from "prop-types";
import {appStore} from "../stores";
import {switchThemes} from "../actions";
export class ThemeSwitchView extends React.Component {
export class ThemeSwitchView extends Component {
static get propTypes() {
return {
theme: React.PropTypes.string,
alternativeTheme: React.PropTypes.string
theme: PropTypes.string,
alternativeTheme: PropTypes.string
};
}

View File

@ -6,22 +6,26 @@ User login and logout controller
@author Dorian Pula [dorian.pula@amber-penguin-software.ca]
*/
/* eslint-disable react/jsx-handler-names */
import React from "react";
import {Modal, Button, Alert, FormControl, Form, ControlLabel, FormGroup, Col} from "react-bootstrap";
import {h, Component} from "preact"; /** @jsx h*/
import {ModalBody, ModalFooter, ModalHeader, Button, Alert, Input, Form, Label, FormGroup, Col} from "reactstrap";
import Modal from "../vendor/Modal";
import FontAwesome from "react-fontawesome";
import {connect} from "react-redux";
import PropTypes from "prop-types";
import {loginUser, logoutUser} from "../actions";
import {appStore} from "../stores";
export class UserLoginView extends React.Component {
export class UserLoginView extends Component {
static get propTypes() {
return {
isUserLoggedIn: React.PropTypes.bool,
fullName: React.PropTypes.string,
externalError: React.PropTypes.string,
isUserLoggedIn: PropTypes.bool,
fullName: PropTypes.string,
externalError: PropTypes.string,
};
}
@ -106,7 +110,7 @@ export class UserLoginView extends React.Component {
let errorDisplay = "";
if (this.state.errorMessage !== undefined && this.state.errorMessage !== "") {
errorDisplay = (<Alert bsStyle="danger">{ this.state.errorMessage }</Alert>);
errorDisplay = (<Alert color="danger">{ this.state.errorMessage }</Alert>);
}
let loginButtonDisabled = this.state.username === "" || this.state.password === "";
@ -122,39 +126,39 @@ export class UserLoginView extends React.Component {
&nbsp;
Do you want to { loginMessage }?
</div>
<Modal show={ this.state.displayLoginModal } onHide={ this.handleHideLoginModal } onKeyPress={ this.handleKeyboardLogin }>
<Modal.Header closeButton>
<Modal.Title>User Login</Modal.Title>
</Modal.Header>
<Modal isOpen={ this.state.displayLoginModal } toggle={ this.handleHideLoginModal } onKeyPress={ this.handleKeyboardLogin }>
<ModalHeader toggle={ this.handleHideLoginModal }>
User Login
</ModalHeader>
<Modal.Body>
<ModalBody>
{errorDisplay}
<Form horizontal>
<FormGroup controlId="username">
<Col xs={ 2 }>
<ControlLabel>Username</ControlLabel>
<FormGroup row>
<Col xs="2">
<Label for="username">Username</Label>
</Col>
<Col xs={ 10 }>
<FormControl type="text" placeholder="Username"
<Col xs="10" >
<Input id="username" type="text" placeholder="Username"
value={ this.state.username } onChange={ this.handleUsernameUpdate } />
</Col>
</FormGroup>
<FormGroup controlId="password">
<Col xs={ 2 }>
<ControlLabel>Password</ControlLabel>
<FormGroup row>
<Col xs="2">
<Label for="password">Password</Label>
</Col>
<Col xs={ 10 }>
<FormControl type="password" placeholder="***"
<Col xs="10">
<Input id="password" type="password" placeholder="***"
value={ this.state.password } onChange={ this.handlePasswordUpdate } />
</Col>
</FormGroup>
</Form>
</Modal.Body>
</ModalBody>
<Modal.Footer>
<Button bsStyle="warning" onClick={ this.handleHideLoginModal }>Cancel</Button>
<Button onClick={ this.handleLoginAttempt } disabled={ loginButtonDisabled }>Login</Button>
</Modal.Footer>
<ModalFooter>
<Button color="warning" onClick={ this.handleHideLoginModal }>Cancel</Button>
<Button color="primary" onClick={ this.handleLoginAttempt } disabled={ loginButtonDisabled }>Login</Button>
</ModalFooter>
</Modal>
</div>
);
@ -165,7 +169,7 @@ export class UserLoginView extends React.Component {
export const ConnectedUserLogin = connect(
(state) => {
return {
"fullName": state.user.fullName,
"fullName": state.user.profile !== undefined ? state.user.profile.fullName : 'Stranger',
"isUserLoggedIn": state.user.token !== "",
"externalError": state.user.accountState
}

View File

@ -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");

View File

@ -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

View File

@ -7,9 +7,8 @@
* @version: 0.5.0
* @license: AGPL v3
*/
// Bootstrap - Needs to be imported first.
@import (inline) "../../node_modules/bootstrap/dist/css/bootstrap.css";
@import (inline) "../../node_modules/bootstrap/dist/css/bootstrap-theme.css";
/* Bootstrap - Needs to be imported first. */
@import url("~bootstrap/dist/css/bootstrap.css");
/*** Declare font-faces from openfontlibrary ***/
@font-face {
@ -21,7 +20,7 @@
@font-face {
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-style: normal;
}
@ -48,35 +47,47 @@
}
/*** Fonts ***/
@header-title-font-family: "BonvenoCFLight", sans-serif;
@console-font-family: "ConsolaMono", monospace;
@amber-penguin-yellow: #FFC200;
:root {
--header-title-font-family: "BonvenoCFLight", sans-serif;
--console-font-family: "ConsolaMono", monospace;
--amber-penguin-yellow: #FFC200;
}
/*** General containers ***/
.sea-header {
text-align: center;
vertical-align: middle;
color: @amber-penguin-yellow;
color: var(--amber-penguin-yellow);
padding: 15px 0;
margin-bottom: 15px;
margin-right: -30px;
margin-top: 5px;
.shadowed_border;
/* mixin: .shadowed_border;*/
border-radius: 25px;
border: 1px solid #111111;
box-shadow: 5px 5px #111111;
}
.ice-floe {
.shadowed_border;
/* mixin: .shadowed_border;*/
border-radius: 25px;
border: 1px solid #111111;
box-shadow: 5px 5px #111111;
padding: 15px 30px 15px 30px;
margin-left: 15px;
margin-right: 5px;
}
.ice-menu {
.shadowed_border;
/* mixin: .shadowed_border;*/
border-radius: 25px;
border: 1px solid #111111;
box-shadow: 5px 5px #111111;
padding: 10px;
margin-bottom: 15px;
}
@ -87,11 +98,11 @@
}
h1, h2 {
font-family: @header-title-font-family;
font-family: var(--header-title-font-family);
}
.sea-header .header_title {
color: @amber-penguin-yellow;
color: var(--amber-penguin-yellow);
vertical-align: middle;
text-align: center;
@ -103,7 +114,7 @@ h1, h2 {
.sea-header .header_tagline {
font-size: 12pt;
font-family: @console-font-family;
font-family: var(--console-font-family);
text-align: center;
color: #eeeeee;
}
@ -112,13 +123,17 @@ h1, h2 {
a,
.hoverable {
text-decoration: none;
.small_bordered;
/* mixin: .small_bordered; */
border-radius: 5px;
padding: 5px;
}
a:hover,
.hoverable:hover {
font-weight: bold;
.small_bordered;
/* mixin: .small_bordered; */
border-radius: 5px;
padding: 5px;
}
ul.navigation-bar {
@ -127,29 +142,33 @@ ul.navigation-bar {
text-align: center;
font-weight: bold;
font-family: @header-title-font-family;
font-family: var(--header-title-font-family);
font-size: 18px;
}
ul.navigation-bar > li > a {
.small_bordered;
/* mixin: .small_bordered;*/
border-radius: 5px;
padding: 5px;
}
/*** Navigation overrides. ***/
.nav {
text-align: center;
font-weight: bold;
font-family: @header-title-font-family;
font-family: var(--header-title-font-family);
font-size: 18px;
}
.nav > li {
.small_bordered;
/* mixin: .small_bordered; */
border-radius: 5px;
padding: 5px;
}
/*** Footer ***/
footer {
font-family: @console-font-family;
font-family: var(--console-font-family);
font-size: small;
font-weight: bold;
text-align: center;
@ -172,9 +191,7 @@ form > ul > li {
.shadowed_border {
border-radius: 25px;
border-style: solid;
border-width: 1px;
border-color: #111111;
border: 1px solid #111111;
box-shadow: 5px 5px #111111;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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");

View File

@ -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";

View File

@ -26,21 +26,35 @@ def config(ctx):
@inv.task
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
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)
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.
:param ctx: Context of the invoke task.
: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)

View File

@ -64,14 +64,12 @@ def js_style(ctx):
: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(run_eslint, echo=True, pty=True)
inv.run('npm run lint', echo=True, pty=True)
@inv.task
def js(ctx):
run_mocha = 'BABEL_DISABLE_CACHE=yes node_modules/mocha/bin/mocha --require tests/babelhook tests/unit'
inv.run(run_mocha, echo=True, pty=True)
inv.run('npm run test', echo=True, pty=True)
@inv.task

View File

@ -5,14 +5,13 @@
<link rel="icon" type="image/icon" href="{{ site['favicon'] }}"/>
<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/css/rookeries-bundle.css"/>
<link rel="stylesheet" type="text/css" media="screen" href="/static/js/rookeries.css"/>
</head>
<body class="evening">
<div id="ui-target">
{{ react_render | safe }}
</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="site_name" value="{{ site['name'] }}"/>
</body>

View File

@ -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>

View File

@ -1,4 +1,4 @@
require("babel-register")({
presets: ["es2015", "react", "stage-0"],
presets: ["es2017", "es2015", "react", "stage-0"],
plugins: ["babel-plugin-rewire"]
});

View File

@ -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>
);
});
});
});

View File

@ -7,16 +7,16 @@ Feature: Site Access
Scenario: Any user can get an existing site
Given I am a visitor
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 landing page
Scenario: An admin can get an existing site
Given I am an authenticated admin user
And I get the site
Then I get a valid site
#Scenario: An admin can get an existing site
# Given I am an authenticated admin user
# And I get the site
# Then I get a valid site
Scenario: An editor can get an existing site
Given I am an authenticated editor user
And I get the site
Then I get a valid site
#Scenario: An editor can get an existing site
# Given I am an authenticated editor user
# And I get the site
# Then I get a valid site

View File

@ -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.');
});
});
});

101
webpack.config.js Normal file
View File

@ -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)
})
]
};

5124
yarn.lock

File diff suppressed because it is too large Load Diff