Merge branch 'dev' into feature/fmp_to_postgresql_t17
No files matched your search
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Issue Template
|
||||
about: A template for new ZenHub stories
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
As a <!-- [developer / logged-in-user] -->
|
||||
I want to <!-- Ex: be able to visit the /login page -->
|
||||
So that <!-- Ex: I can login to my account -->
|
||||
|
||||
### Acceptance Criteria:
|
||||
The following needs to be completed for this story to be "done":
|
||||
- [ ] ...
|
||||
|
||||
### Notes:
|
||||
Helpful information to complete the task, suggestions on how to do so:
|
||||
- ...
|
||||
@@ -0,0 +1,13 @@
|
||||
### Description:
|
||||
Closes issue # ...
|
||||
|
||||
|
||||
### Notes:
|
||||
|
||||
|
||||
### Screenshots (Optional):
|
||||
|
||||
|
||||
### Checklist:
|
||||
- [ ] 90% Test Coverage
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
name: Build project and Run Lighthouse CI
|
||||
on: [push]
|
||||
jobs:
|
||||
lhci:
|
||||
name: Lighthouse CI
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Use Node.js 10.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
- name: npm install, build
|
||||
run: |
|
||||
pushd pwa && npm install
|
||||
npm run build && popd
|
||||
- name: run Lighthouse CI
|
||||
run: |
|
||||
npm install -g @lhci/cli@0.3.x
|
||||
lhci autorun || echo "LHCI failed!"
|
||||
- name: Upload lighthouse report
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: lighthouse
|
||||
path: ./.lighthouseci
|
||||
@@ -16,8 +16,10 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: pushd pwa && npm install && popd
|
||||
- run: pushd pwa && npm run build && popd
|
||||
- run: pushd pwa && npm test && popd
|
||||
- run: docker-compose -f dev-docker-compose.yml up --detach
|
||||
- run: docker exec haiti_pwa npm run test:cov
|
||||
- run: docker exec haiti_pwa npm run cy:run
|
||||
- run: docker exec haiti_pwa npm run build
|
||||
- run: docker-compose -f dev-docker-compose.yml down
|
||||
env:
|
||||
CI: true
|
||||
@@ -1,3 +1,6 @@
|
||||
**/.lighthouseci/**
|
||||
env/*
|
||||
.vscode/*
|
||||
# postgres persisted data
|
||||
pgdata/
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"ci": {
|
||||
"collect": {
|
||||
"staticDistDir": "./pwa/build",
|
||||
"numberOfRuns": 3
|
||||
},
|
||||
"upload": {
|
||||
"target": "temporary-public-storage"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
The application runs on docker, which can be installed [here](https://docs.docker.com/install/).
|
||||
If you're a linux user, you'll also need to install [docker-compose](https://docs.docker.com/compose/install/) separately. Otherwise, it's included in the Mac and PC versions of "Docker Desktop"
|
||||
|
||||
- The PWA and accompanying database can be started by running the command `docker-compose up` from the root directory.
|
||||
- `docker-compose down` will then stop the docker processes from running in the background.
|
||||
- Once the images have been built and the processes started, the PWA can be seen at your localhost, at the port specified in [docker-compose.yml](https://github.com/Haiti-Distributed-Healthcare-System/hdhs/blob/dev/docker-compose.yml).
|
||||
- The PWA, API, Database, and Swagger documentation can be started by running the command `docker-compose -f dev-docker-compose.yml up` from the root directory.
|
||||
- `docker-compose -f dev-docker-compose.yml down` will then stop the docker processes from running in the background.
|
||||
- Once the images have been built and the processes started, the PWA can be seen at your localhost, at the port specified in [dev-docker-compose.yml](https://github.com/Haiti-Distributed-Healthcare-System/hdhs/blob/dev/dev-docker-compose.yml).
|
||||
- More tips to troubleshoot docker are available [here](https://github.com/Haiti-Distributed-Healthcare-System/hdhs/wiki/Using-Docker).
|
||||
@@ -0,0 +1,59 @@
|
||||
version: '3'
|
||||
services:
|
||||
pwa:
|
||||
build:
|
||||
context: ./pwa
|
||||
volumes:
|
||||
# These dirs can be changed outside of the conatiner, and reflected within.
|
||||
# All other files require a rebuild:
|
||||
# docker-compose -f dev-docker-compose.yml up --build
|
||||
- ./pwa/src:/data/src
|
||||
- ./pwa/cypress:/data/cypress
|
||||
- ./pwa/public:/data/public
|
||||
- ./pwa/coverage:/data/coverage
|
||||
ports:
|
||||
- "3000:3000"
|
||||
container_name: haiti_pwa
|
||||
environment:
|
||||
environment: "docker"
|
||||
|
||||
database:
|
||||
image: "postgres" # use latest official postgres version
|
||||
volumes:
|
||||
- "./pgdata:/var/lib/postgresql/data" # persist data even if container shuts down
|
||||
ports:
|
||||
- "5432:5432"
|
||||
expose:
|
||||
- "5422"
|
||||
environment:
|
||||
POSTGRES_DB: app_db
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: HaitiCHI
|
||||
environment: "docker"
|
||||
|
||||
api:
|
||||
image: postgrest/postgrest
|
||||
ports:
|
||||
- "4000:4000"
|
||||
expose:
|
||||
- "4000"
|
||||
links:
|
||||
- database:database
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://postgres:HaitiCHI@database:5432/app_db
|
||||
PGRST_DB_SCHEMA: public
|
||||
PGRST_DB_ANON_ROLE: postgres
|
||||
PGRST_SERVER_PORT: 4000
|
||||
environment: "docker"
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
swagger:
|
||||
image: swaggerapi/swagger-ui
|
||||
ports:
|
||||
- "4001:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
environment:
|
||||
API_URL: http://localhost:4000
|
||||
environment: "docker"
|
||||
@@ -1,11 +1,14 @@
|
||||
version: '3'
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
pwa:
|
||||
build:
|
||||
context: ./pwa
|
||||
dockerfile: Dockerfile-pi
|
||||
volumes:
|
||||
- ./pwa/:/app
|
||||
- type: bind
|
||||
source: ./pwa/build
|
||||
target: /data/app/build
|
||||
ports:
|
||||
- "3000:3000"
|
||||
container_name: haiti_pwa
|
||||
@@ -13,10 +16,11 @@ services:
|
||||
database:
|
||||
image: "postgres" # use latest official postgres version
|
||||
env_file:
|
||||
- ./db/database.env # configure postgres
|
||||
- ./database.env # configure postgres
|
||||
volumes:
|
||||
- database-data:/var/lib/postgresql/data/ # persist data even if container shuts down
|
||||
- "database-data:/var/lib/postgresql/data/" # persist data even if container shuts down
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "5432:5432"
|
||||
|
||||
volumes:
|
||||
database-data: # named volumes can be managed easier using docker-compose
|
||||
database-data: # named volumes can be managed easier using docker-compose
|
||||
@@ -8,9 +8,6 @@
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
FROM node:12.10.0
|
||||
|
||||
COPY package*.json ./data/
|
||||
FROM cypress/base:12.16.0
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
ENV PATH /data/node_modules/.bin:$PATH
|
||||
|
||||
COPY . /data/app
|
||||
WORKDIR /data/app
|
||||
COPY . ./
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM node:12.10.0
|
||||
|
||||
COPY package*.json ./data/
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
RUN npm install
|
||||
|
||||
ENV PATH /data/node_modules/.bin:$PATH
|
||||
|
||||
COPY . /data/app
|
||||
WORKDIR /data/app
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "run", "build"]
|
||||
@@ -0,0 +1 @@
|
||||
{"video": false}
|
||||
@@ -0,0 +1,22 @@
|
||||
# Cypress.io end-to-end tests
|
||||
|
||||
[Cypress.io](https://www.cypress.io) is an open source, MIT licensed end-to-end test runner
|
||||
|
||||
## Folder structure
|
||||
|
||||
These folders hold end-to-end tests and supporting files for the Cypress Test Runner.
|
||||
|
||||
- [fixtures](fixtures) holds optional JSON data for mocking, [read more](https://on.cypress.io/fixture)
|
||||
- [integration](integration) holds the actual test files, [read more](https://on.cypress.io/writing-and-organizing-tests)
|
||||
- [plugins](plugins) allow you to customize how tests are loaded, [read more](https://on.cypress.io/plugins)
|
||||
- [support](support) file runs before all tests and is a great place to write or load additional custom commands, [read more](https://on.cypress.io/writing-and-organizing-tests#Support-file)
|
||||
|
||||
## `cypress.json` file
|
||||
|
||||
You can configure project options in the [../cypress.json](../cypress.json) file, see [Cypress configuration doc](https://on.cypress.io/configuration).
|
||||
|
||||
## More information
|
||||
|
||||
- [https://github.com/cypress.io/cypress](https://github.com/cypress.io/cypress)
|
||||
- [https://docs.cypress.io/](https://docs.cypress.io/)
|
||||
- [Writing your first Cypress test](http://on.cypress.io/intro)
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// enables intelligent code completion for Cypress commands
|
||||
// https://on.cypress.io/intelligent-code-completion
|
||||
/// <reference types="cypress" />
|
||||
|
||||
describe('Homepage', () => {
|
||||
it('shows login button', function () {
|
||||
cy.visit('http://localhost:3000')
|
||||
cy.get('.am-button').contains('Login')
|
||||
})
|
||||
})
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const webpack = require('@cypress/webpack-preprocessor');
|
||||
|
||||
const webpackOptions = {
|
||||
mode: 'development',
|
||||
// make sure the source maps work
|
||||
devtool: 'eval-source-map',
|
||||
// webpack will transpile TS and JS files
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.tsx', '.jsx'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env', '@babel/preset-react'],
|
||||
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
loader: ['babel-loader', 'awesome-typescript-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: [/node_modules/],
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
exclude: [/node_modules/],
|
||||
use: ['svg-url-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const options = {
|
||||
webpackOptions,
|
||||
watchOptions: {},
|
||||
};
|
||||
|
||||
module.exports = (on, config) => {
|
||||
on('file:preprocessor', webpack(options));
|
||||
|
||||
return config;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"../node_modules/cypress",
|
||||
"*/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"strict": false,
|
||||
"baseUrl": "../node_modules",
|
||||
"target": "es5",
|
||||
"lib": ["es7", "dom"],
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react",
|
||||
"inlineSourceMap": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.0.5",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.4.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
@@ -10,6 +11,8 @@
|
||||
"@types/node": "^12.12.26",
|
||||
"@types/react": "^16.9.19",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"antd-mobile": "^2.3.1",
|
||||
"node-sass": "^4.13.1",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-scripts": "3.3.1",
|
||||
@@ -19,11 +22,24 @@
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"cy:open": "cypress open",
|
||||
"cy:run": "cypress run",
|
||||
"test:cov": "react-scripts test --coverage --watchAll=false",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"jest": {
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"statements": 90
|
||||
}
|
||||
},
|
||||
"coverageReporters": [
|
||||
"lcov"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
@@ -35,5 +51,17 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/webpack-preprocessor": "^4.1.3",
|
||||
"@testing-library/react-hooks": "^3.2.1",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-plugin-import": "^1.13.0",
|
||||
"cypress": "^4.2.0",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"svg-url-loader": "^5.0.0",
|
||||
"ts-loader": "^6.2.2",
|
||||
"webpack": "^4.42.1"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 333 KiB |
|
Before Width: | Height: | Size: 497 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 28 KiB |
@@ -9,7 +9,7 @@
|
||||
name="description"
|
||||
content="Healthcare Application for the Haiti Community Healthcare Initiative"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/appIcon512.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
@@ -24,7 +24,7 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>HDHS Patient Tool</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 55 KiB |
@@ -7,42 +7,7 @@
|
||||
"start_url": ".",
|
||||
"icons": [
|
||||
{
|
||||
"src": "images/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icons/icon-512x512.png",
|
||||
"src": "appIcon512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,6 @@ import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText(/learn react/i);
|
||||
const linkElement = getByText(/Login/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -1,26 +1,15 @@
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
import React from "react";
|
||||
import "./scss/App.scss";
|
||||
import "./antd-mobile.css";
|
||||
|
||||
import Login from "./components/Login";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
<>
|
||||
<Login />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,10 @@
|
||||
/*!
|
||||
*
|
||||
* antd-mobile v2.3.1
|
||||
*
|
||||
* Copyright 2015-present, Alipay, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
*/
|
||||
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit;font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}[hidden],template{display:none}.am-fade-appear,.am-fade-enter{opacity:0}.am-fade-appear,.am-fade-enter,.am-fade-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-timing-function:cubic-bezier(.55,0,.55,.2);animation-timing-function:cubic-bezier(.55,0,.55,.2);-webkit-animation-play-state:paused;animation-play-state:paused}.am-fade-appear.am-fade-appear-active,.am-fade-enter.am-fade-enter-active{-webkit-animation-name:amFadeIn;animation-name:amFadeIn;-webkit-animation-play-state:running;animation-play-state:running}.am-fade-leave.am-fade-leave-active{-webkit-animation-name:amFadeOut;animation-name:amFadeOut;-webkit-animation-play-state:running;animation-play-state:running}@-webkit-keyframes amFadeIn{0%{opacity:0}to{opacity:1}}@keyframes amFadeIn{0%{opacity:0}to{opacity:1}}@-webkit-keyframes amFadeOut{0%{opacity:1}to{opacity:0}}@keyframes amFadeOut{0%{opacity:1}to{opacity:0}}.am-slide-up-appear,.am-slide-up-enter{-webkit-transform:translateY(100%);-ms-transform:translateY(100%);transform:translateY(100%)}.am-slide-up-appear,.am-slide-up-enter,.am-slide-up-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-timing-function:cubic-bezier(.55,0,.55,.2);animation-timing-function:cubic-bezier(.55,0,.55,.2);-webkit-animation-play-state:paused;animation-play-state:paused}.am-slide-up-appear.am-slide-up-appear-active,.am-slide-up-enter.am-slide-up-enter-active{-webkit-animation-name:amSlideUpIn;animation-name:amSlideUpIn;-webkit-animation-play-state:running;animation-play-state:running}.am-slide-up-leave.am-slide-up-leave-active{-webkit-animation-name:amSlideUpOut;animation-name:amSlideUpOut;-webkit-animation-play-state:running;animation-play-state:running}@-webkit-keyframes amSlideUpIn{0%{-webkit-transform:translateY(100%);transform:translateY(100%)}to{-webkit-transform:translate(0);transform:translate(0)}}@keyframes amSlideUpIn{0%{-webkit-transform:translateY(100%);transform:translateY(100%)}to{-webkit-transform:translate(0);transform:translate(0)}}@-webkit-keyframes amSlideUpOut{0%{-webkit-transform:translate(0);transform:translate(0)}to{-webkit-transform:translateY(100%);transform:translateY(100%)}}@keyframes amSlideUpOut{0%{-webkit-transform:translate(0);transform:translate(0)}to{-webkit-transform:translateY(100%);transform:translateY(100%)}}.am.am-zoom-enter,.am.am-zoom-leave{display:block}.am-zoom-appear,.am-zoom-enter{opacity:0;-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:boLine truncated
|
||||
/*# sourceMappingURL=antd-mobile.min.css.map*/
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import LandingPage from "./LandingPage";
|
||||
|
||||
test("renders learn react link", () => {
|
||||
const { getByText } = render(<LandingPage />);
|
||||
const lookupButtonElement = getByText(/Patient Lookup/i);
|
||||
const newPatientButtonElement = getByText(/New Patient/i);
|
||||
expect(lookupButtonElement).toBeInTheDocument();
|
||||
expect(newPatientButtonElement).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { ReactElement } from "react";
|
||||
import { WhiteSpace, Button } from "antd-mobile";
|
||||
import Icon, { UserAddOutlined } from "@ant-design/icons";
|
||||
|
||||
import "../scss/LandingPage.scss";
|
||||
|
||||
export default function LandingPage(): ReactElement {
|
||||
/* istanbul ignore next */
|
||||
const patientLookup = () => {
|
||||
console.log("Clicked Patient Lookup");
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
const newPatient = () => {
|
||||
console.log("Clicked New Patient");
|
||||
};
|
||||
return (
|
||||
<div id="landing-wrapper">
|
||||
<div className="button-flex">
|
||||
<Button onClick={patientLookup} icon="search">
|
||||
Patient Lookup
|
||||
</Button>
|
||||
<WhiteSpace />
|
||||
</div>
|
||||
<br />
|
||||
<div className="button-flex">
|
||||
<Button icon={<UserAddOutlined />} onClick={newPatient}>
|
||||
New Patient
|
||||
</Button>
|
||||
<WhiteSpace />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import useApi from '../hooks/useApi'
|
||||
import {InputItem, Button} from 'antd-mobile'
|
||||
import '../scss/Login.scss'
|
||||
|
||||
export default function Login(): ReactElement {
|
||||
const {response, error, isLoading} = useApi("")
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="loginWrapper">
|
||||
<div id="loginGrid">
|
||||
<div id="iconCard">
|
||||
<div id="imgWrap">
|
||||
<img src="/logoIcon512.jpg" alt="CHI Logo"></img>
|
||||
</div>
|
||||
</div>
|
||||
<InputItem placeholder="username"/>
|
||||
<InputItem placeholder="password" type="password"/>
|
||||
<div id="buttonFlex">
|
||||
<Button>Login</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>Loading API Fetch: {isLoading.toString()}</p>
|
||||
{response &&
|
||||
<p>API Fetch Result: {JSON.stringify(response)}</p>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { renderHook } from '@testing-library/react-hooks'
|
||||
import useApi from './useApi'
|
||||
|
||||
test('should return loading and then a response', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() => useApi(""))
|
||||
|
||||
expect(result.current.response).toBeNull()
|
||||
expect(result.current.error).toBeNull()
|
||||
expect(result.current.isLoading).toBeTruthy()
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(result.current.response).toBeTruthy()
|
||||
expect(result.current.error).toBeNull()
|
||||
expect(result.current.isLoading).toBeFalsy()
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
|
||||
const useApi = (route: string) => {
|
||||
const [response, setResponse] = React.useState(null);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
/* istanbul ignore next */
|
||||
const getUrl = () => {
|
||||
if (process.env.environment === "docker"){
|
||||
return "http://api:4000/"
|
||||
}
|
||||
else {
|
||||
return "http://localhost:4000/"
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch(getUrl() + route);
|
||||
const json = await res.json();
|
||||
setResponse(json);
|
||||
setIsLoading(false)
|
||||
} catch (error) {
|
||||
setError(error);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
return { response, error, isLoading };
|
||||
};
|
||||
|
||||
export default useApi
|
||||
@@ -1,6 +1,7 @@
|
||||
/* istanbul ignore file */
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import './scss/index.scss';
|
||||
import App from './App';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,7 @@
|
||||
.am-button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100vh;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.button-flex {
|
||||
animation-name: fade-in;
|
||||
animation-duration: 2s;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 2em 0;
|
||||
.am-button {
|
||||
background-color: #9bd7ff;
|
||||
width: 40%;
|
||||
&.am-button-active {
|
||||
background-color: #ccebff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#landing-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// @import '_include-media.scss';
|
||||
// @include media("<desktop") {
|
||||
|
||||
#loginWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
height: 100%;
|
||||
padding: 0 2rem;
|
||||
#loginGrid {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 45%;
|
||||
#iconCard {
|
||||
animation-name: fade-in-from-up;
|
||||
animation-duration: 1.5s;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
#imgWrap {
|
||||
width: 28%;
|
||||
margin: auto;
|
||||
padding: .2rem;
|
||||
background-color: #eee;
|
||||
border-radius: 5px;
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 512px;
|
||||
height: auto;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
input {
|
||||
animation-name: fade-in;
|
||||
animation-duration: 2s;
|
||||
}
|
||||
#buttonFlex{
|
||||
animation-name: fade-in;
|
||||
animation-duration: 2s;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.am-button {
|
||||
background-color: #9BD7FF;
|
||||
width: 40%;
|
||||
&.am-button-active {
|
||||
background-color: #ccebff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$animationDelay: 1;
|
||||
@for $i from 1 through 15 {
|
||||
.loginGrid :nth-child(#{$i}) {
|
||||
animation-delay: #{0.3+($i)/30}s;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fade-in-from-up {
|
||||
0% { opacity: 0; transform: translateY(-100px)}
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
// _ _ _ _ _
|
||||
// (_) | | | | | (_)
|
||||
// _ _ __ ___| |_ _ __| | ___ _ __ ___ ___ __| |_ __ _
|
||||
// | | '_ \ / __| | | | |/ _` |/ _ \ | '_ ` _ \ / _ \/ _` | |/ _` |
|
||||
// | | | | | (__| | |_| | (_| | __/ | | | | | | __/ (_| | | (_| |
|
||||
// |_|_| |_|\___|_|\__,_|\__,_|\___| |_| |_| |_|\___|\__,_|_|\__,_|
|
||||
//
|
||||
// Simple, elegant and maintainable media queries in Sass
|
||||
// v1.4.9
|
||||
//
|
||||
// https://eduardoboucas.github.io/include-media
|
||||
//
|
||||
// Authors: Eduardo Boucas (@eduardoboucas)
|
||||
// Hugo Giraudel (@hugogiraudel)
|
||||
//
|
||||
// This project is licensed under the terms of the MIT license
|
||||
////
|
||||
/// include-media library public configuration
|
||||
/// @author Eduardo Boucas
|
||||
/// @access public
|
||||
////
|
||||
|
||||
|
||||
///
|
||||
/// Creates a list of global breakpoints
|
||||
///
|
||||
/// @example scss - Creates a single breakpoint with the label `phone`
|
||||
/// $breakpoints: ('phone': 320px);
|
||||
///
|
||||
$breakpoints: (
|
||||
'phone': 320px,
|
||||
'tablet': 768px,
|
||||
'desktop': 1024px
|
||||
) !default;
|
||||
|
||||
|
||||
///
|
||||
/// Creates a list of static expressions or media types
|
||||
///
|
||||
/// @example scss - Creates a single media type (screen)
|
||||
/// $media-expressions: ('screen': 'screen');
|
||||
///
|
||||
/// @example scss - Creates a static expression with logical disjunction (OR operator)
|
||||
/// $media-expressions: (
|
||||
/// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)'
|
||||
/// );
|
||||
///
|
||||
$media-expressions: (
|
||||
'screen': 'screen',
|
||||
'print': 'print',
|
||||
'handheld': 'handheld',
|
||||
'landscape': '(orientation: landscape)',
|
||||
'portrait': '(orientation: portrait)',
|
||||
'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx)',
|
||||
'retina3x': '(-webkit-min-device-pixel-ratio: 3), (min-resolution: 350dpi), (min-resolution: 3dppx)'
|
||||
) !default;
|
||||
|
||||
|
||||
///
|
||||
/// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals
|
||||
///
|
||||
/// @example scss - Interval for pixels is defined as `1` by default
|
||||
/// @include media('>128px') {}
|
||||
///
|
||||
/// /* Generates: */
|
||||
/// @media (min-width: 129px) {}
|
||||
///
|
||||
/// @example scss - Interval for ems is defined as `0.01` by default
|
||||
/// @include media('>20em') {}
|
||||
///
|
||||
/// /* Generates: */
|
||||
/// @media (min-width: 20.01em) {}
|
||||
///
|
||||
/// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;`
|
||||
/// @include media('>2.0rem') {}
|
||||
///
|
||||
/// /* Generates: */
|
||||
/// @media (min-width: 2.1rem) {}
|
||||
///
|
||||
$unit-intervals: (
|
||||
'px': 1,
|
||||
'em': 0.01,
|
||||
'rem': 0.1,
|
||||
'': 0
|
||||
) !default;
|
||||
|
||||
///
|
||||
/// Defines whether support for media queries is available, useful for creating separate stylesheets
|
||||
/// for browsers that don't support media queries.
|
||||
///
|
||||
/// @example scss - Disables support for media queries
|
||||
/// $im-media-support: false;
|
||||
/// @include media('>=tablet') {
|
||||
/// .foo {
|
||||
/// color: tomato;
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// /* Generates: */
|
||||
/// .foo {
|
||||
/// color: tomato;
|
||||
/// }
|
||||
///
|
||||
$im-media-support: true !default;
|
||||
|
||||
///
|
||||
/// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or
|
||||
/// intercept the breakpoint will be displayed, any others will be ignored.
|
||||
///
|
||||
/// @example scss - This media query will show because it intercepts the static breakpoint
|
||||
/// $im-media-support: false;
|
||||
/// $im-no-media-breakpoint: 'desktop';
|
||||
/// @include media('>=tablet') {
|
||||
/// .foo {
|
||||
/// color: tomato;
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// /* Generates: */
|
||||
/// .foo {
|
||||
/// color: tomato;
|
||||
/// }
|
||||
///
|
||||
/// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint
|
||||
/// $im-media-support: false;
|
||||
/// $im-no-media-breakpoint: 'tablet';
|
||||
/// @include media('>=desktop') {
|
||||
/// .foo {
|
||||
/// color: tomato;
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// /* No output */
|
||||
///
|
||||
$im-no-media-breakpoint: 'desktop' !default;
|
||||
|
||||
///
|
||||
/// Selects which media expressions are allowed in an expression for it to be used when media queries
|
||||
/// are not supported.
|
||||
///
|
||||
/// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions
|
||||
/// $im-media-support: false;
|
||||
/// $im-no-media-breakpoint: 'desktop';
|
||||
/// $im-no-media-expressions: ('screen');
|
||||
/// @include media('>=tablet', 'screen') {
|
||||
/// .foo {
|
||||
/// color: tomato;
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// /* Generates: */
|
||||
/// .foo {
|
||||
/// color: tomato;
|
||||
/// }
|
||||
///
|
||||
/// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted
|
||||
/// $im-media-support: false;
|
||||
/// $im-no-media-breakpoint: 'desktop';
|
||||
/// $im-no-media-expressions: ('screen');
|
||||
/// @include media('>=tablet', 'retina2x') {
|
||||
/// .foo {
|
||||
/// color: tomato;
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// /* No output */
|
||||
///
|
||||
$im-no-media-expressions: ('screen', 'portrait', 'landscape') !default;
|
||||
|
||||
////
|
||||
/// Cross-engine logging engine
|
||||
/// @author Hugo Giraudel
|
||||
/// @access private
|
||||
////
|
||||
|
||||
|
||||
///
|
||||
/// Log a message either with `@error` if supported
|
||||
/// else with `@warn`, using `feature-exists('at-error')`
|
||||
/// to detect support.
|
||||
///
|
||||
/// @param {String} $message - Message to log
|
||||
///
|
||||
@function im-log($message) {
|
||||
@if feature-exists('at-error') {
|
||||
@error $message;
|
||||
} @else {
|
||||
@warn $message;
|
||||
$_: noop();
|
||||
}
|
||||
|
||||
@return $message;
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
/// Wrapper mixin for the log function so it can be used with a more friendly
|
||||
/// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function
|
||||
/// within functions because it is not possible to include a mixin in a function
|
||||
/// and use the mixin everywhere else because it's much more elegant.
|
||||
///
|
||||
/// @param {String} $message - Message to log
|
||||
///
|
||||
@mixin log($message) {
|
||||
@if im-log($message) {}
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
/// Function with no `@return` called next to `@warn` in Sass 3.3
|
||||
/// to trigger a compiling error and stop the process.
|
||||
///
|
||||
@function noop() {}
|
||||
|
||||
///
|
||||
/// Determines whether a list of conditions is intercepted by the static breakpoint.
|
||||
///
|
||||
/// @param {Arglist} $conditions - Media query conditions
|
||||
///
|
||||
/// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint
|
||||
///
|
||||
@function im-intercepts-static-breakpoint($conditions...) {
|
||||
$no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint);
|
||||
|
||||
@if not $no-media-breakpoint-value {
|
||||
@if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {}
|
||||
}
|
||||
|
||||
@each $condition in $conditions {
|
||||
@if not map-has-key($media-expressions, $condition) {
|
||||
$operator: get-expression-operator($condition);
|
||||
$prefix: get-expression-prefix($operator);
|
||||
$value: get-expression-value($condition, $operator);
|
||||
|
||||
@if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or
|
||||
($prefix == 'min' and $value > $no-media-breakpoint-value) {
|
||||
@return false;
|
||||
}
|
||||
} @else if not index($im-no-media-expressions, $condition) {
|
||||
@return false;
|
||||
}
|
||||
}
|
||||
|
||||
@return true;
|
||||
}
|
||||
|
||||
////
|
||||
/// Parsing engine
|
||||
/// @author Hugo Giraudel
|
||||
/// @access private
|
||||
////
|
||||
|
||||
|
||||
///
|
||||
/// Get operator of an expression
|
||||
///
|
||||
/// @param {String} $expression - Expression to extract operator from
|
||||
///
|
||||
/// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤`
|
||||
///
|
||||
@function get-expression-operator($expression) {
|
||||
@each $operator in ('>=', '>', '<=', '<', '≥', '≤') {
|
||||
@if str-index($expression, $operator) {
|
||||
@return $operator;
|
||||
}
|
||||
}
|
||||
|
||||
// It is not possible to include a mixin inside a function, so we have to
|
||||
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
|
||||
// functions cannot be called anywhere in Sass, we need to hack the call in
|
||||
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
|
||||
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
|
||||
$_: im-log('No operator found in `#{$expression}`.');
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
/// Get dimension of an expression, based on a found operator
|
||||
///
|
||||
/// @param {String} $expression - Expression to extract dimension from
|
||||
/// @param {String} $operator - Operator from `$expression`
|
||||
///
|
||||
/// @return {String} - `width` or `height` (or potentially anything else)
|
||||
///
|
||||
@function get-expression-dimension($expression, $operator) {
|
||||
$operator-index: str-index($expression, $operator);
|
||||
$parsed-dimension: str-slice($expression, 0, $operator-index - 1);
|
||||
$dimension: 'width';
|
||||
|
||||
@if str-length($parsed-dimension) > 0 {
|
||||
$dimension: $parsed-dimension;
|
||||
}
|
||||
|
||||
@return $dimension;
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
/// Get dimension prefix based on an operator
|
||||
///
|
||||
/// @param {String} $operator - Operator
|
||||
///
|
||||
/// @return {String} - `min` or `max`
|
||||
///
|
||||
@function get-expression-prefix($operator) {
|
||||
@return if(index(('<', '<=', '≤'), $operator), 'max', 'min');
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
/// Get value of an expression, based on a found operator
|
||||
///
|
||||
/// @param {String} $expression - Expression to extract value from
|
||||
/// @param {String} $operator - Operator from `$expression`
|
||||
///
|
||||
/// @return {Number} - A numeric value
|
||||
///
|
||||
@function get-expression-value($expression, $operator) {
|
||||
$operator-index: str-index($expression, $operator);
|
||||
$value: str-slice($expression, $operator-index + str-length($operator));
|
||||
|
||||
@if map-has-key($breakpoints, $value) {
|
||||
$value: map-get($breakpoints, $value);
|
||||
} @else {
|
||||
$value: to-number($value);
|
||||
}
|
||||
|
||||
$interval: map-get($unit-intervals, unit($value));
|
||||
|
||||
@if not $interval {
|
||||
// It is not possible to include a mixin inside a function, so we have to
|
||||
// rely on the `im-log(..)` function rather than the `log(..)` mixin. Because
|
||||
// functions cannot be called anywhere in Sass, we need to hack the call in
|
||||
// a dummy variable, such as `$_`. If anybody ever raise a scoping issue with
|
||||
// Sass 3.3, change this line in `@if im-log(..) {}` instead.
|
||||
$_: im-log('Unknown unit `#{unit($value)}`.');
|
||||
}
|
||||
|
||||
@if $operator == '>' {
|
||||
$value: $value + $interval;
|
||||
} @else if $operator == '<' {
|
||||
$value: $value - $interval;
|
||||
}
|
||||
|
||||
@return $value;
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
/// Parse an expression to return a valid media-query expression
|
||||
///
|
||||
/// @param {String} $expression - Expression to parse
|
||||
///
|
||||
/// @return {String} - Valid media query
|
||||
///
|
||||
@function parse-expression($expression) {
|
||||
// If it is part of $media-expressions, it has no operator
|
||||
// then there is no need to go any further, just return the value
|
||||
@if map-has-key($media-expressions, $expression) {
|
||||
@return map-get($media-expressions, $expression);
|
||||
}
|
||||
|
||||
$operator: get-expression-operator($expression);
|
||||
$dimension: get-expression-dimension($expression, $operator);
|
||||
$prefix: get-expression-prefix($operator);
|
||||
$value: get-expression-value($expression, $operator);
|
||||
|
||||
@return '(#{$prefix}-#{$dimension}: #{$value})';
|
||||
}
|
||||
|
||||
///
|
||||
/// Slice `$list` between `$start` and `$end` indexes
|
||||
///
|
||||
/// @access private
|
||||
///
|
||||
/// @param {List} $list - List to slice
|
||||
/// @param {Number} $start [1] - Start index
|
||||
/// @param {Number} $end [length($list)] - End index
|
||||
///
|
||||
/// @return {List} Sliced list
|
||||
///
|
||||
@function slice($list, $start: 1, $end: length($list)) {
|
||||
@if length($list) < 1 or $start > $end {
|
||||
@return ();
|
||||
}
|
||||
|
||||
$result: ();
|
||||
|
||||
@for $i from $start through $end {
|
||||
$result: append($result, nth($list, $i));
|
||||
}
|
||||
|
||||
@return $result;
|
||||
}
|
||||
|
||||
////
|
||||
/// String to number converter
|
||||
/// @author Hugo Giraudel
|
||||
/// @access private
|
||||
////
|
||||
|
||||
|
||||
///
|
||||
/// Casts a string into a number
|
||||
///
|
||||
/// @param {String | Number} $value - Value to be parsed
|
||||
///
|
||||
/// @return {Number}
|
||||
///
|
||||
@function to-number($value) {
|
||||
@if type-of($value) == 'number' {
|
||||
@return $value;
|
||||
} @else if type-of($value) != 'string' {
|
||||
$_: im-log('Value for `to-number` should be a number or a string.');
|
||||
}
|
||||
|
||||
$first-character: str-slice($value, 1, 1);
|
||||
$result: 0;
|
||||
$digits: 0;
|
||||
$minus: ($first-character == '-');
|
||||
$numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9);
|
||||
|
||||
// Remove +/- sign if present at first character
|
||||
@if ($first-character == '+' or $first-character == '-') {
|
||||
$value: str-slice($value, 2);
|
||||
}
|
||||
|
||||
@for $i from 1 through str-length($value) {
|
||||
$character: str-slice($value, $i, $i);
|
||||
|
||||
@if not (index(map-keys($numbers), $character) or $character == '.') {
|
||||
@return to-length(if($minus, -$result, $result), str-slice($value, $i))
|
||||
}
|
||||
|
||||
@if $character == '.' {
|
||||
$digits: 1;
|
||||
} @else if $digits == 0 {
|
||||
$result: $result * 10 + map-get($numbers, $character);
|
||||
} @else {
|
||||
$digits: $digits * 10;
|
||||
$result: $result + map-get($numbers, $character) / $digits;
|
||||
}
|
||||
}
|
||||
|
||||
@return if($minus, -$result, $result);
|
||||
}
|
||||
|
||||
|
||||
///
|
||||
/// Add `$unit` to `$value`
|
||||
///
|
||||
/// @param {Number} $value - Value to add unit to
|
||||
/// @param {String} $unit - String representation of the unit
|
||||
///
|
||||
/// @return {Number} - `$value` expressed in `$unit`
|
||||
///
|
||||
@function to-length($value, $unit) {
|
||||
$units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax);
|
||||
|
||||
@if not index(map-keys($units), $unit) {
|
||||
$_: im-log('Invalid unit `#{$unit}`.');
|
||||
}
|
||||
|
||||
@return $value * map-get($units, $unit);
|
||||
}
|
||||
|
||||
///
|
||||
/// This mixin aims at redefining the configuration just for the scope of
|
||||
/// the call. It is helpful when having a component needing an extended
|
||||
/// configuration such as custom breakpoints (referred to as tweakpoints)
|
||||
/// for instance.
|
||||
///
|
||||
/// @author Hugo Giraudel
|
||||
///
|
||||
/// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints`
|
||||
/// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression`
|
||||
///
|
||||
/// @example scss - Extend the global breakpoints with a tweakpoint
|
||||
/// @include media-context(('custom': 678px)) {
|
||||
/// .foo {
|
||||
/// @include media('>phone', '<=custom') {
|
||||
/// // ...
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// @example scss - Extend the global media expressions with a custom one
|
||||
/// @include media-context($tweak-media-expressions: ('all': 'all')) {
|
||||
/// .foo {
|
||||
/// @include media('all', '>phone') {
|
||||
/// // ...
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// @example scss - Extend both configuration maps
|
||||
/// @include media-context(('custom': 678px), ('all': 'all')) {
|
||||
/// .foo {
|
||||
/// @include media('all', '>phone', '<=custom') {
|
||||
/// // ...
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
@mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) {
|
||||
// Save global configuration
|
||||
$global-breakpoints: $breakpoints;
|
||||
$global-media-expressions: $media-expressions;
|
||||
|
||||
// Update global configuration
|
||||
$breakpoints: map-merge($breakpoints, $tweakpoints) !global;
|
||||
$media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global;
|
||||
|
||||
@content;
|
||||
|
||||
// Restore global configuration
|
||||
$breakpoints: $global-breakpoints !global;
|
||||
$media-expressions: $global-media-expressions !global;
|
||||
}
|
||||
|
||||
////
|
||||
/// include-media public exposed API
|
||||
/// @author Eduardo Boucas
|
||||
/// @access public
|
||||
////
|
||||
|
||||
|
||||
///
|
||||
/// Generates a media query based on a list of conditions
|
||||
///
|
||||
/// @param {Arglist} $conditions - Media query conditions
|
||||
///
|
||||
/// @example scss - With a single set breakpoint
|
||||
/// @include media('>phone') { }
|
||||
///
|
||||
/// @example scss - With two set breakpoints
|
||||
/// @include media('>phone', '<=tablet') { }
|
||||
///
|
||||
/// @example scss - With custom values
|
||||
/// @include media('>=358px', '<850px') { }
|
||||
///
|
||||
/// @example scss - With set breakpoints with custom values
|
||||
/// @include media('>desktop', '<=1350px') { }
|
||||
///
|
||||
/// @example scss - With a static expression
|
||||
/// @include media('retina2x') { }
|
||||
///
|
||||
/// @example scss - Mixing everything
|
||||
/// @include media('>=350px', '<tablet', 'retina3x') { }
|
||||
///
|
||||
@mixin media($conditions...) {
|
||||
@if ($im-media-support and length($conditions) == 0) or
|
||||
(not $im-media-support and im-intercepts-static-breakpoint($conditions...)) {
|
||||
@content;
|
||||
} @else if ($im-media-support and length($conditions) > 0) {
|
||||
@media #{unquote(parse-expression(nth($conditions, 1)))} {
|
||||
// Recursive call
|
||||
@include media(slice($conditions, 2)...) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,12 @@ body {
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #e8ffd9;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
/* istanbul ignore file */
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"strictNullChecks": false,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||