Sets up basic Angular app

This commit is contained in:
Aaron Louie 2020-09-22 14:33:50 -04:00
parent bb79833833
commit 6a81c96c12
127 changed files with 17697 additions and 84 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

121
.gitignore vendored
View File

@ -1,88 +1,49 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# dependencies
/node_modules
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# Coverage directory used by tools like istanbul
coverage
*.lcov
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# nyc test coverage
.nyc_output
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# Bower dependency directory (https://bower.io/)
bower_components
# System Files
.DS_Store
Thumbs.db
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# SonarCloud
/.scannerwork/

8
.sonarcloud.properties Normal file
View File

@ -0,0 +1,8 @@
sonar.organization=sartography
sonar.projectKey=sartography_cr-connect-frontend
sonar.host.url=https://sonarcloud.io
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.typescript.tsconfigPath=tsconfig.json
sonar.coverage.exclusions=**/*.mocks.ts, **/*.spec.ts, e2e/**, src/assets/**, src/main.ts, src/test.ts, src/polyfills.ts, karma.conf.js, docker/**
sonar.cpd.exclusions=**/*.mocks.ts, **/*.spec.ts, e2e/**, src/assets/**, src/main.ts, src/test.ts, src/polyfills.ts, karma.conf.js, docker/**
sonar.exclusions=src/assets/**

55
.travis.yml Normal file
View File

@ -0,0 +1,55 @@
sudo: required
dist: bionic
language: node_js
node_js:
- 12
services:
- docker
- xvfb
before_install:
- |
if [[ $TRAVIS_BRANCH =~ ^(feature\/.*)$ ]]; then
export E2E_TAG="dev"
elif [[ $TRAVIS_BRANCH =~ ^(dev|testing|demo|training|staging|rrt\/.*)$ ]]; then
export E2E_TAG="${TRAVIS_BRANCH//\//_}"
else
export E2E_TAG="latest"
fi
echo "E2E_TAG = $E2E_TAG"
install:
- npm install
addons:
chrome: stable
env:
global:
- API_URL=http://localhost:5000/v1.0
- BASE_HREF=/
- DEPLOY_URL=/
- HOME_ROUTE=home
- IRB_URL=http://localhost:5001/
- PORT0=4200
- PRODUCTION=false
script:
- npm run ci
deploy:
provider: script
script: bash ./deploy.sh sartography/cr-connect-frontend
on:
all_branches: true
condition: $TRAVIS_BRANCH =~ ^(dev|testing|demo|training|staging|master|rrt\/.*)$
notifications:
email:
on_success: change
on_failure: always
recipients:
- dan@sartography.com

31
Dockerfile Normal file
View File

@ -0,0 +1,31 @@
### STAGE 1: Build ###
FROM sartography/cr-connect-angular-base AS builder
COPY . /app/
ARG build_config=prod
RUN npm install && \
npm run build:$build_config
### STAGE 2: Run ###
FROM nginx:alpine
RUN set -x && apk add --update --no-cache bash libintl gettext curl
COPY --from=builder /app/dist/* /etc/nginx/html/
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
# Script for substituting environment variables
COPY ./docker/substitute-env-variables.sh ./entrypoint.sh
RUN chmod +x ./entrypoint.sh
# Fix for Angular routing
RUN echo "pushstate: enabled" > /etc/nginx/html/Staticfile
# The entrypoint.sh script will run after the container finishes starting.
# Substitutes environment variables in nginx configuration and index.html,
# then starts/reloads nginx.
ENTRYPOINT ["./entrypoint.sh", \
"/etc/nginx/html/index.html,/etc/nginx/conf.d/default.conf", \
"PRODUCTION,API_URL,IRB_URL,HOME_ROUTE,BASE_HREF,DEPLOY_URL,PORT0,GOOGLE_ANALYTICS_KEY,SENTRY_KEY,TITLE", \
"/etc/nginx/html", \
"true"]

View File

@ -1,7 +1,6 @@
MIT License
The MIT License (MIT)
Copyright (c) 2020 Aaron Louie
Copyright (c) 2019 Sartography
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -1 +1,44 @@
covid19-testing-kiosk
# sartography/cr-connect-frontend
[![Build Status](https://travis-ci.com/sartography/cr-connect-frontend.svg?branch=master)](https://travis-ci.com/sartography/cr-connect-frontend)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=sartography_cr-connect-frontend&metric=coverage)](https://sonarcloud.io/dashboard?id=sartography_cr-connect-frontend)
# CR Connect Frontend
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.15.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Local Development with Sartogprahy Libraries
If you are making changes to the Sartography Libraries dependency,
you can replace that line in the package.json file with something akin to this, where you supply the full path to the dist folder, remember
to run 'npm build' in that directory for your local changes to take effect, and be sure to change this back to the proper value before
committing your code
```
"sartography-workflow-lib": "/home/dan/code/workflow/sartography-libraries/dist/sartography-workflow-lib",
```
Also note that you need to add
```json
"preserveSymlinks": true
```
to your angular.json file in build/options.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

176
angular.json Normal file
View File

@ -0,0 +1,176 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"cr-connect-frontend": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/cr-connect-frontend",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
]
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
},
"staging": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
},
"test": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "cr-connect-frontend:build"
},
"configurations": {
"production": {
"browserTarget": "cr-connect-frontend:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "cr-connect-frontend:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": [
"node_modules/marked/lib/marked.js"
],
"codeCoverageExclude": [
"src/polyfills.ts",
"src/test.ts",
"docker/**/*"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "cr-connect-frontend:serve"
},
"configurations": {
"production": {
"devServerTarget": "cr-connect-frontend:serve:production"
}
}
}
}
}
},
"defaultProject": "cr-connect-frontend",
"cli": {
"analytics": "39bf12a6-4921-4c8d-bd17-8df6300cf98a"
}
}

12
browserslist Normal file
View File

@ -0,0 +1,12 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.

45
deploy.sh Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash
#########################################################################
# Builds the Docker image for the current git branch on Travis CI and
# publishes it to Docker Hub.
#
# Parameters:
# $1: Docker Hub repository to publish to
#
# Required environment variables (place in Settings menu on Travis CI):
# $DOCKER_USERNAME: Docker Hub username
# $DOCKER_TOKEN: Docker Hub access token
#########################################################################
echo 'Building Docker image...'
DOCKER_REPO="$1"
function branch_to_tag () {
if [ "$1" == "master" ]; then echo "latest"; else echo "$1" ; fi
}
function branch_to_deploy_group() {
if [[ $1 =~ ^(rrt\/.*)$ ]]; then echo "rrt"; else echo "crconnect" ; fi
}
DOCKER_TAG=$(branch_to_tag "$TRAVIS_BRANCH")
DEPLOY_GROUP=$(branch_to_deploy_group "$TRAVIS_BRANCH")
if [ "$DEPLOY_GROUP" == "rrt" ]; then
IFS='/' read -ra ARR <<< "$TRAVIS_BRANCH" # Split branch on '/' character
DOCKER_TAG=$(branch_to_tag "rrt_${ARR[1]}")
fi
echo "DOCKER_REPO = $DOCKER_REPO"
echo "DOCKER_TAG = $DOCKER_TAG"
echo "$DOCKER_TOKEN" | docker login -u "$DOCKER_USERNAME" --password-stdin || exit 1
docker build -f Dockerfile -t "$DOCKER_REPO:$DOCKER_TAG" . || exit 1
# Push Docker image to Docker Hub
echo "Publishing to Docker Hub..."
docker push "$DOCKER_REPO" || exit 1
echo "Done."

95
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,95 @@
version: "3.3"
services:
db:
container_name: db
image: sartography/cr-connect-db:$E2E_TAG
ports:
- "5432:5432"
environment:
- POSTGRES_USER=crc_user
- POSTGRES_PASSWORD=crc_pass
- POSTGRES_MULTIPLE_DATABASES=crc_test,pb_test
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 10
pb:
container_name: pb
depends_on:
- db
image: sartography/protocol-builder-mock:$E2E_TAG
environment:
- APPLICATION_ROOT=/
- CORS_ALLOW_ORIGINS=localhost:5000,backend:5000,localhost:5002,bpmn:5002,localhost:4200,frontend:4200
- DB_HOST=db
- DB_NAME=pb_test
- DB_PASSWORD=crc_pass
- DB_PORT=5432
- DB_USER=crc_user
- PORT0=5001
- UPGRADE_DB=true
ports:
- "5001:5001"
command: ./wait-for-it.sh db:5432 -t 0 -- ./docker_run.sh
backend:
container_name: backend
depends_on:
- db
- pb
image: sartography/cr-connect-workflow:$E2E_TAG
environment:
- APPLICATION_ROOT=/
- CORS_ALLOW_ORIGINS=localhost:5002,bpmn:5002,localhost:4200,frontend:4200
- DB_HOST=db
- DB_NAME=crc_test
- DB_PASSWORD=crc_pass
- DB_PORT=5432
- DB_USER=crc_user
- DEVELOPMENT=true
- LDAP_URL=mock
- PB_BASE_URL=http://pb:5001/v2.0/
- PB_ENABLED=true
- PORT0=5000
- PRODUCTION=false
- RESET_DB=true
- TESTING=false
- UPGRADE_DB=true
ports:
- "5000:5000"
command: ./wait-for-it.sh pb:5001 -t 0 -- ./docker_run.sh
# bpmn:
# container_name: bpmn
# depends_on:
# - db
# - backend
# image: sartography/cr-connect-bpmn:dev
# environment:
# - API_URL=http://localhost:5000/api/v1.0
# - BASE_HREF=/bpmn/
# - DEPLOY_URL=/bpmn/
# - HOME_ROUTE=home
# - PORT0=5002
# - PRODUCTION=false
# ports:
# - "5002:5002"
#
# frontend:
# container_name: frontend
# depends_on:
# - db
# - backend
# image: sartography/cr-connect-frontend:dev
# environment:
# - API_URL=http://localhost:5000/api/v1.0
# - BASE_HREF=/app/
# - DEPLOY_URL=/app/
# - HOME_ROUTE=home
# - PORT0=4200
# - PRODUCTION=false
# ports:
# - "4200:4200"

View File

@ -0,0 +1,123 @@
#!/bin/bash
#####################################################################
# Substitutes the given environment variables in the given files.
# Parameters:
# $1: Comma-delimited list of file paths
# $2: Comma-delimited list of environment variables
# $3: Absolute path to nginx html directory (optional)
# $4: Should restart nginx (optional)
#####################################################################
echo 'Substituting environment variables...'
num_args=0
# The first parameter is a comma-delimited list of paths to files which should be substituted
if [[ -z $1 ]]; then
echo 'ERROR: No target files given.'
exit 1
else
num_args=1
fi
# The second parameter is a comma-delimited list of environment variable names
if [[ -z $2 ]]; then
echo 'ERROR: No environment variables given.'
exit 1
else
num_args=2
fi
# The third parameter is the absolute path to the nginx html directory
if [[ -z $3 ]]; then
echo '' # It's optional. Don't print anything.
else
num_args=3
fi
# The fourth parameter, if 'true', is whether we should reload nginx
if [[ -z $4 ]]; then
echo '' # It's optional. Don't print anything.
else
num_args=4
fi
# Find & replace BASE_HREF in all files in the nginx html directory
if [[ "$2" == *"BASE_HREF"* ]] && [[ "$2" == *"DEPLOY_URL"* ]]; then
# Add trailing slash to $BASE_HREF if needed
length=${#BASE_HREF}
last_char=${BASE_HREF:length-1:1}
[[ $last_char != "/" ]] && BASE_HREF="$BASE_HREF/"; :
# Add trailing slash to $DEPLOY_URL if needed
length=${#DEPLOY_URL}
last_char=${DEPLOY_URL:length-1:1}
[[ $last_char != "/" ]] && DEPLOY_URL="$DEPLOY_URL/"; :
# The third parameter is the absolute path to the nginx html directory
if [[ $num_args -ge 3 ]]; then
# Replace all instances of __REPLACE_ME_WITH_BASE_HREF__ with $BASE_HREF
find "$3" \( -type d -name .git -prune \) -o -type f -print0 | \
xargs -0 sed -i 's@__REPLACE_ME_WITH_BASE_HREF__@'"$BASE_HREF"'@g'
echo 'Replacing base href...'
# Wait a few seconds in case find | sed needs more time
sleep 3
# Replace all instances of __REPLACE_ME_WITH_DEPLOY_URL__ with $DEPLOY_URL
find "$3" \( -type d -name .git -prune \) -o -type f -print0 | \
xargs -0 sed -i 's@__REPLACE_ME_WITH_DEPLOY_URL__@'"$DEPLOY_URL"'@g'
echo 'Replacing deploy url...'
# Wait a few seconds in case find | sed needs more time
sleep 3
fi
fi
# Convert "VAR1,VAR2,VAR3,..." to "\$VAR1 \$VAR2 \$VAR3 ..."
env_list="\\\$${2//,/ \\\$}" # "\" and "$" are escaped as "\\" and "\$"
for file_path in ${1//,/ }
do
echo "replacing environment variables in $file_path"
# Replace strings in the given file(s) in env_list
envsubst "$env_list" < "$file_path" > "$file_path".tmp && mv "$file_path".tmp "$file_path"
echo '...'
# Wait a second in case envsubst needs more time
sleep 1
# If this is the nginx default.conf file, replace double slashes with single slashes
if [[ $file_path == *"/default.conf"* ]]; then
sed -i -e 's@//@/@g' "$file_path"
fi
done
echo 'Finished substituting environment variables.'
for env_var in ${2//,/ }
do
echo "$env_var = ${!env_var}"
done
# Reload nginx
if [ $num_args -ge 4 ] && [ "$4" == "true" ]; then
# Check to see if nginx command is available
if hash nginx 2> /dev/null; then
# Check to see if nginx is already running
if [ -e /var/run/nginx.pid ]; then
echo "nginx is currently running. Reloading nginx..."
exec nginx -s reload
echo "nginx reloaded."
else
echo "nginx is not yet running. Starting nginx..."
exec nginx -g 'daemon off;'
echo "nginx started."
fi
else
echo "nginx command not found on this system."
fi
fi
# Execute all other commands with parameters
num_args=$((num_args + 1))
exec "${@:num_args}"

32
e2e/protractor.conf.js Normal file
View File

@ -0,0 +1,32 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 60000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 60000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

125
e2e/src/app.e2e-spec.ts Normal file
View File

@ -0,0 +1,125 @@
import {HttpClient} from 'protractor-http-client';
import {AppPage} from './app.po';
describe('Clinical Research Coordinator App', () => {
let page: AppPage;
let http: HttpClient;
beforeEach(() => {
page = new AppPage();
http = new HttpClient('http://localhost:5001');
});
it('should automatically sign-in and redirect to home screen', () => {
page.navigateTo();
expect(page.getRoute()).toEqual('/home');
expect(page.getElements('#cta_protocol_builder').count()).toBeGreaterThan(0);
});
it('should navigate to help screen', () => {
page.clickAndExpectRoute('#nav_help', '/help');
});
it('should navigate to profile', () => {
page.clickElement('#nav_account');
page.clickAndExpectRoute('#nav_profile', '/profile');
});
it('should navigate to notifications', () => {
page.clickElement('#nav_account');
page.clickAndExpectRoute('#nav_notifications', '/notifications');
});
it('should navigate back to home screen', () => {
page.setLocalStorageVar('numstudy', '1');
expect(page.getLocalStorageVar('numstudy')).toEqual('1');
page.clickAndExpectRoute('#nav_home', '/home');
expect(page.getElements('#cta_protocol_builder').count()).toBeGreaterThan(0);
});
it('should open Protocol Builder in new window', async () => {
expect(page.getElements('#cta_protocol_builder').count()).toEqual(1);
expect(page.getElements('#cta_reload_studies').count()).toEqual(1);
// Open Protocol Builder in new tab.
const numTabsBefore = await page.getNumTabs();
page.clickElement('#cta_protocol_builder');
const numTabsAfter = await page.getNumTabs();
expect(numTabsAfter).toBeGreaterThan(numTabsBefore);
// Close Protocol Builder tab.
await page.switchFocusToTab(1);
await page.closeTab();
await page.switchFocusToTab(0);
});
it('should load new study from Protocol Builder', async () => {
const numStudiesBefore = await page.getElements('.study-row').count();
// Add a new study to Protocol Builder.
http.post('/new_study', '' +
`STUDYID=${Math.floor(Math.random() * 100000)}&` +
`TITLE=${encodeURIComponent('New study title')}&` +
`NETBADGEID=dhf8r&` +
`DATE_MODIFIED=${encodeURIComponent(new Date().toISOString())}&` +
`requirements=9&` +
`requirements=21&` +
`requirements=40&` +
`requirements=44&` +
`requirements=52&` +
`requirements=53&` +
`Q_COMPLETE=y`,
{'Content-Type': 'application/x-www-form-urlencoded'}
).catch(error => {
console.error(error);
});
http.failOnHttpError = false;
// Reload the list of studies.
await page.clickElement('#cta_reload_studies');
await page.waitForNotVisible('.loading');
await page.waitForClickable('.study-row');
const numStudiesAfter = await page.getElements('.study-row').count();
expect(numStudiesAfter).toBeGreaterThan(numStudiesBefore);
});
it('should navigate to a study', async () => {
const studyRow = page.getElement('.study-row');
const studyId = await studyRow.getAttribute('data-study-id');
await expect(studyId).not.toBeUndefined();
await expect(studyId).not.toBeNull();
page.clickAndExpectRoute('.study-row', '/study/' + studyId);
});
it('should display workflow spec categories in tiles', async () => {
const numTiles = await page.getElements('.workflow-list-item').count();
expect(numTiles).toBeGreaterThan(0);
});
it('should navigate to a workflow', async () => {
const wfSelector = '.workflow-list-item .workflow-action';
expect(page.getElements(wfSelector).count()).toBeGreaterThan(0);
const workflow = await page.getElement(wfSelector);
const studyId = await workflow.getAttribute('data-study-id');
const catId = await workflow.getAttribute('data-category-id');
const workflowId = await workflow.getAttribute('data-workflow-id');
console.log('studyId', studyId);
console.log('catId', catId);
console.log('workflowId', workflowId);
const expectedRoute = `/study/${studyId}?category=${catId}&workflow=${workflowId}`;
await page.clickElement(wfSelector);
const newRoute = await page.getRoute();
expect(newRoute.slice(0, expectedRoute.length)).toEqual(expectedRoute);
});
// TODO: CATCH 401/403 ERRORS AND VERIFY THAT THEY REDIRECT TO LOGIN
// afterEach(async () => {
// // Assert that there are no errors emitted from the browser
// const logs = await browser.manage().logs().get(logging.Type.BROWSER);
// expect(logs).not.toContain(jasmine.objectContaining({
// level: logging.Level.SEVERE,
// } as logging.Entry));
// });
});

103
e2e/src/app.po.ts Normal file
View File

@ -0,0 +1,103 @@
import {browser, by, element, ElementArrayFinder, ElementFinder, ExpectedConditions} from 'protractor';
export class AppPage {
navigateTo() {
return browser.get(browser.baseUrl) as Promise<any>;
}
clickAndExpectRoute(clickSelector: string, expectedRoute: string | RegExp) {
this.waitForClickable(clickSelector);
this.clickElement(clickSelector);
if (typeof expectedRoute === 'string') {
expect(this.getRoute()).toEqual(expectedRoute);
} else {
expect(this.getRoute()).toMatch(expectedRoute);
}
}
clickElement(selector: string) {
this.waitForClickable(selector);
this.scrollTo(selector);
this.focus(selector);
return this.getElement(selector).click();
}
closeTab() {
return browser.close();
}
focus(selector: string) {
return browser.controlFlow().execute(() => {
return browser.executeScript('arguments[0].focus()', this.getElement(selector).getWebElement());
});
}
getElement(selector: string): ElementFinder {
return element.all(by.css(selector)).first();
}
getElements(selector: string): ElementArrayFinder {
return element.all(by.css(selector));
}
getLocalStorageVar(name: string) {
return browser.executeScript(`return window.localStorage.getItem('${name}');`);
}
getNumTabs() {
return browser.getAllWindowHandles().then(wh => {
return wh.length;
});
}
async getRoute() {
const url = await this.getUrl();
return '/' + url.split(browser.baseUrl)[1];
}
getText(selector: string) {
return element(by.css(selector)).getText() as Promise<string>;
}
getUrl() {
return browser.getCurrentUrl();
}
scrollTo(selector: string) {
browser.controlFlow().execute(() => {
browser.executeScript('arguments[0].scrollIntoView(false)', this.getElement(selector).getWebElement());
});
}
setLocalStorageVar(name: string, value: string) {
return browser.executeScript(`return window.localStorage.setItem('${name}','${value}');`);
}
switchFocusToTab(tabIndex: number) {
return browser.getAllWindowHandles().then(wh => {
return wh.forEach((h, i) => {
if (i === tabIndex) { return browser.switchTo().window(h); }
});
});
}
waitFor(t: number) {
return browser.sleep(t);
}
waitForClickable(selector: string) {
const e = this.getElement(selector);
return browser.wait(ExpectedConditions.elementToBeClickable(e), 5000);
}
waitForNotVisible(selector: string) {
const e = this.getElement(selector);
return browser.wait(ExpectedConditions.invisibilityOf(e), 5000);
}
waitForVisible(selector: string) {
const e = this.getElement(selector);
return browser.wait(ExpectedConditions.visibilityOf(e), 5000);
}
}

13
e2e/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

44
karma.conf.js Normal file
View File

@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
if (config.browsers.indexOf('ChromeHeadlessCI') !== -1) {
process.env.CHROME_BIN = require('puppeteer').executablePath();
}
config.set({
files: ['karma.globals.js'],
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage'),
reports: ['lcovonly'],
fixWebpackSourcePaths: true
},
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox', '--disable-gpu' ]
}
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

6
karma.globals.js Normal file
View File

@ -0,0 +1,6 @@
var ENV = {
production: 'false',
api: 'apiRoot',
irbUrl: 'irbUrl',
homeRoute: 'home',
};

12
nginx.conf Normal file
View File

@ -0,0 +1,12 @@
server {
listen $PORT0;
port_in_redirect off;
location $BASE_HREF/ {
alias /etc/nginx/html/;
index index.html index.htm;
try_files $uri$args $uri $BASE_HREF/index.html;
}
}

13713
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

69
package.json Normal file
View File

@ -0,0 +1,69 @@
{
"name": "cr-connect-frontend",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"build:prod": "ng build --configuration=production --prod --base-href=__REPLACE_ME_WITH_BASE_HREF__ --deploy-url=__REPLACE_ME_WITH_DEPLOY_URL__",
"build:staging": "ng build --configuration=staging --prod --base-href=__REPLACE_ME_WITH_BASE_HREF__ --deploy-url=__REPLACE_ME_WITH_DEPLOY_URL__",
"build:test": "ng build --configuration=test",
"test": "ng test",
"test:coverage": "ng test --codeCoverage=true --watch=false --browsers=ChromeHeadless",
"lint": "ng lint",
"e2e": "./node_modules/protractor/bin/webdriver-manager update && ng e2e",
"e2e:with-backend": "npm run backend && ng e2e && npm run backend:stop",
"backend:stop": "cd docker && docker-compose down && cd ..",
"backend:build": "cd docker && docker-compose pull && docker-compose build && cd ..",
"backend:start": "cd docker && docker-compose up -d --force-recreate && cd ..",
"backend": "npm run backend:stop && npm run backend:build && npm run backend:start",
"env": "chmod +x ./docker/substitute-env-variables.sh && ./docker/substitute-env-variables.sh src/index.html PRODUCTION,API_URL,IRB_URL,HOME_ROUTE,BASE_HREF,DEPLOY_URL,PORT0,GOOGLE_ANALYTICS_KEY,SENTRY_KEY,TITLE",
"ci": "npm run lint && npm run test:coverage && sonar-scanner && npm run env && npm run backend && npm run e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~9.1.11",
"@angular/cdk": "^9.2.4",
"@angular/common": "~9.1.11",
"@angular/compiler": "~9.1.11",
"@angular/core": "~9.1.11",
"@angular/flex-layout": "^9.0.0-beta.31",
"@angular/forms": "~9.1.11",
"@angular/material": "^9.2.4",
"@angular/platform-browser": "~9.1.11",
"@angular/platform-browser-dynamic": "~9.1.11",
"@angular/router": "~9.1.11",
"@ngx-formly/core": "^5.8.0",
"@ngx-formly/material": "^5.8.0",
"lodash.isequal": "^4.5.0",
"rfdc": "^1.1.4",
"rxjs": "~6.5.4",
"tslib": "^1.13.0",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.901.10",
"@angular/cli": "^9.1.10",
"@angular/compiler-cli": "~9.1.11",
"@angular/language-service": "~9.1.11",
"@types/jasmine": "^3.5.11",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.12.47",
"codelyzer": "^5.1.2",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "^5.1.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~2.1.1",
"karma-jasmine": "~3.1.1",
"karma-jasmine-html-reporter": "^1.5.4",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"protractor": "^7.0.0",
"protractor-http-client": "^1.0.4",
"sonar-scanner": "^3.1.0",
"ts-node": "~8.6.2",
"tslint": "~6.0.0",
"typescript": "~3.7.5"
}
}

8
sonar-project.properties Normal file
View File

@ -0,0 +1,8 @@
sonar.organization=sartography
sonar.projectKey=sartography_cr-connect-frontend
sonar.host.url=https://sonarcloud.io
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.typescript.tsconfigPath=tsconfig.json
sonar.coverage.exclusions=**/*.mocks.ts, **/*.spec.ts, e2e/**, src/assets/**, src/main.ts, src/test.ts, src/polyfills.ts, karma.conf.js, docker/**
sonar.cpd.exclusions=**/*.mocks.ts, **/*.spec.ts, e2e/**, src/assets/**, src/main.ts, src/test.ts, src/polyfills.ts, karma.conf.js, docker/**
sonar.exclusions=src/assets/**

65
src/_config.scss Normal file
View File

@ -0,0 +1,65 @@
// GLOBAL SCSS VARIABLES
$body-font-family: 'franklin-gothic-urw', sans-serif;
$heading-font-family: 'franklin-gothic-urw-cond', sans-serif;
// COLOR PALETTE
// gray
$brand-gray: #4e4e4e;
$brand-gray-tint-1: #666666;
$brand-gray-tint-2: #DADADA;
$brand-gray-tint-3: #F1F1EF;
$brand-gray-tint-4: scale-color($brand-gray, $lightness: 90%);
$body-color: $brand-gray;
$brand-gray-shade-1: scale-color($brand-gray, $lightness: -20%);
$brand-gray-shade-2: scale-color($brand-gray, $lightness: -40%);
$brand-gray-shade-3: scale-color($brand-gray, $lightness: -60%);
$brand-gray-shade-4: scale-color($brand-gray, $lightness: -80%);
$brand-gray-shade-5: scale-color($brand-gray, $lightness: -100%);
$body-color-muted: $brand-gray-tint-1;
$body-color-light: $brand-gray-tint-4;
$brand-gray-muted: $brand-gray-tint-1;
$brand-gray-light: $brand-gray-tint-4;
// primary (UVA "Jefferson Blue")
$brand-primary: #232D4B;
$brand-primary-tint-1: #394E79;
$brand-primary-tint-2: #6C799C;
$brand-primary-tint-3: #A9AFC7;
$brand-primary-tint-4: scale-color($brand-primary, $lightness: 90%);
$brand-primary-shade-1: #092255;
$brand-primary-shade-2: #041D4F;
$brand-primary-shade-3: #02194A;
$brand-primary-shade-4: #021745;
$brand-primary-shade-5: #03143E;
$brand-primary-muted: $brand-primary-tint-1;
$brand-primary-light: $brand-primary-tint-4;
// accent (UVA "Rotunda Orange")
$brand-accent: #E57200;
$brand-accent-tint-1: #F69350;
$brand-accent-tint-2: #FAB584;
$brand-accent-tint-3: #FDD8BB;
$brand-accent-tint-4: scale-color($brand-accent, $lightness: 90%);
$brand-accent-shade-1: #E76E25;
$brand-accent-shade-2: #DD6923;
$brand-accent-shade-3: #D36421;
$brand-accent-shade-4: #C8601F;
$brand-accent-shade-5: #C05B1D;
$brand-accent-muted: $brand-accent-tint-1;
$brand-accent-light: $brand-accent-tint-4;
// warning (UVA "Emergency Red")
$brand-warning: #DF1E43;
$brand-warning-muted: desaturate(scale-color($brand-warning, $lightness: 30%), 20%);
$brand-warning-light: scale-color($brand-warning, $lightness: 90%);
// Green
$brand-green: #64B343;
$brand-green-muted: #8EC774;
$brand-green-light: #B5D9A3;
// Dimensions
$easeDuration: 300ms;
$animationDuration: 500ms;
$header-height: 84px;

92
src/_material.scss Normal file
View File

@ -0,0 +1,92 @@
@import 'config';
@import '../node_modules/@angular/material/theming';
// Define a custom typography config that overrides the font-family
$custom-typography: mat-typography-config(
$font-family: $body-font-family,
$display-4: mat-typography-level(3.75rem, 1.0925, 700, $body-font-family),
$display-3: mat-typography-level(2.4917rem, 1.1037, 700, $body-font-family),
$display-2: mat-typography-level(2.2148rem, 1.1287, 700, $body-font-family),
$display-1: mat-typography-level(1.9688rem, 1.1429, 700, $body-font-family),
$headline: mat-typography-level(1.7500rem, 1.1429, 700, $body-font-family), // h1
$title: mat-typography-level(1.5625rem, 1.1200, 500, $body-font-family), // h2
$subheading-2: mat-typography-level(1.3750rem, 1.0000, 700, $heading-font-family), // h3
$subheading-1: mat-typography-level(1.2500rem, 1.1000, 700, $body-font-family), // h4
$body-2: mat-typography-level(1.1250rem, 1.1111, 500, $body-font-family), // h5
$body-1: mat-typography-level(1.0000rem, 1.1250, 500, $body-font-family), // p
$caption: mat-typography-level(0.8750rem, 0.7143, 500, $body-font-family), // small
$button: mat-typography-level(1.0000rem, 1.1250, 500, $body-font-family),
$input: mat-typography-level(1.0000rem, 1.1250, 500, $body-font-family)
);
$mat-blue: (
50: #f1f5f7,
100: #b3c1d3,
200: $brand-primary-tint-3,
300: $brand-primary-tint-2,
400: $brand-primary-tint-1,
500: $brand-primary,
600: $brand-primary-shade-1,
700: $brand-primary-shade-2,
800: $brand-primary-shade-3,
900: $brand-primary-shade-4,
A100: #b3c1d3,
A200: $brand-primary-tint-3,
A400: $brand-primary-tint-2,
A700: $brand-primary-tint-1,
contrast: (
50: $body-color,
100: $body-color,
200: $body-color,
300: #ffffff,
400: #ffffff,
500: #ffffff,
600: #ffffff,
700: #ffffff,
800: #ffffff,
900: #ffffff,
A100: $body-color,
A200: #ffffff,
A400: #ffffff,
A700: #ffffff,
)
);
$mat-orange: (
50: #fceee0,
100: $brand-accent-tint-3,
200: $brand-accent-tint-2,
300: $brand-accent-tint-1,
400: $brand-accent,
500: $brand-accent-shade-1,
600: $brand-accent-shade-2,
700: $brand-accent-shade-3,
800: $brand-accent-shade-4,
900: $brand-accent-shade-5,
A100: #fceee0,
A200: $brand-accent-tint-3,
A400: $brand-accent-tint-2,
A700: $brand-accent-tint-1,
contrast: (
50: $body-color,
100: $body-color,
200: $body-color,
300: $body-color,
400: $body-color,
500: #ffffff,
600: #ffffff,
700: #ffffff,
800: #ffffff,
900: #ffffff,
A100: $body-color,
A200: $body-color,
A400: $body-color,
A700: $body-color,
)
);
$cr-connect-primary: mat-palette($mat-blue);
$cr-connect-accent: mat-palette($mat-orange);
$cr-connect-warn: mat-palette($mat-red);
$cr-connect-theme: mat-light-theme($cr-connect-primary, $cr-connect-accent, $cr-connect-warn);

View File

@ -0,0 +1,113 @@
import {APP_BASE_HREF, Location} from '@angular/common';
import {HttpClient} from '@angular/common/http';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {TestBed} from '@angular/core/testing';
import {FormsModule} from '@angular/forms';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatListModule} from '@angular/material/list';
import {MatMenuModule} from '@angular/material/menu';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatSelectModule} from '@angular/material/select';
import {MatSidenavModule} from '@angular/material/sidenav';
import {MatToolbarModule} from '@angular/material/toolbar';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {FormlyModule} from '@ngx-formly/core';
import {FormlyMaterialModule} from '@ngx-formly/material';
import {ChartsModule} from 'ng2-charts';
import {MarkdownModule} from 'ngx-markdown';
import {ApiService, MockEnvironment, SessionRedirectComponent, ToFormlyPipe} from 'sartography-workflow-lib';
import {routes} from './app-routing.module';
import {AppComponent} from './app.component';
import {DashboardComponent} from './dashboard/dashboard.component';
import {FooterComponent} from './footer/footer.component';
import {HelpComponent} from './help/help.component';
import {HomeComponent} from './home/home.component';
import {InboxComponent} from './inbox/inbox.component';
import {NavbarComponent} from './navbar/navbar.component';
import {NotificationsComponent} from './notifications/notifications.component';
import {ProfileComponent} from './profile/profile.component';
import {StudiesComponent} from './studies/studies.component';
import {StudyComponent} from './study/study.component';
import {WorkflowFilesComponent} from './workflow-files/workflow-files.component';
import {WorkflowFormComponent} from './workflow-form/workflow-form.component';
import {WorkflowStepsMenuListComponent} from './workflow-steps-menu-list/workflow-steps-menu-list.component';
import {WorkflowComponent} from './workflow/workflow.component';
describe('Router: App', () => {
let location: Location;
let router: Router;
let fixture;
const mockEnvironment = new MockEnvironment();
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
DashboardComponent,
DashboardComponent,
FooterComponent,
HelpComponent,
HomeComponent,
InboxComponent,
NavbarComponent,
NotificationsComponent,
ProfileComponent,
SessionRedirectComponent,
StudiesComponent,
StudyComponent,
ToFormlyPipe,
WorkflowComponent,
WorkflowFilesComponent,
WorkflowFormComponent,
WorkflowStepsMenuListComponent,
],
imports: [
BrowserAnimationsModule,
ChartsModule,
FormlyMaterialModule,
FormlyModule,
FormsModule,
HttpClientTestingModule,
MarkdownModule,
MatButtonModule,
MatCardModule,
MatFormFieldModule,
MatIconModule,
MatListModule,
MatMenuModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatSelectModule,
MatSidenavModule,
MatToolbarModule,
NoopAnimationsModule,
RouterTestingModule.withRoutes(routes),
],
providers: [
HttpClient,
ApiService,
{provide: 'APP_ENVIRONMENT', useValue: mockEnvironment},
{provide: APP_BASE_HREF, useValue: '/'},
]
});
router = TestBed.inject(Router);
location = TestBed.inject(Location);
fixture = TestBed.createComponent(AppComponent);
fixture.ngZone.run(() => router.initialNavigation());
});
it('navigate to "" redirects you to /', async () => {
console.log('mockEnvironment', mockEnvironment);
const success = await fixture.ngZone.run(() => router.navigate(['']));
expect(success).toBeTruthy();
expect(location.path()).toBe('/home');
});
});

View File

@ -0,0 +1,28 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {ThisEnvironment} from '../environments/environment.injectable';
import {HomeComponent} from './home/home.component';
export const routes: Routes = [
{
path: '',
pathMatch: 'full',
component: HomeComponent
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
scrollOffset: [0, 84],
})
],
exports: [RouterModule],
providers: [
{provide: 'APP_ENVIRONMENT', useClass: ThisEnvironment},
]
})
export class AppRoutingModule {
}

View File

@ -0,0 +1,10 @@
<div class="mat-typography">
<app-navbar (userChanged)="reload()"></app-navbar>
<router-outlet *ngIf="!loading; else loadingMessage"></router-outlet>
<app-footer></app-footer>
</div>
<ng-template #loadingMessage>
<app-loading></app-loading>
</ng-template>

View File

View File

@ -0,0 +1,58 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClient} from '@angular/common/http';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MatIconModule} from '@angular/material/icon';
import {FakeMatIconRegistry} from '@angular/material/icon/testing';
import {MatMenuModule} from '@angular/material/menu';
import {RouterTestingModule} from '@angular/router/testing';
import {AppComponent} from './app.component';
import {FooterComponent} from './footer/footer.component';
import {NavbarComponent} from './navbar/navbar.component';
import {ApiService} from './services/api.service';
import {MockEnvironment} from './testing/environment.mock';
describe('AppComponent', () => {
let component: AppComponent;
let fixture: ComponentFixture<AppComponent>;
const mockEnvironment = new MockEnvironment();
const mockTitle = `'Once,' said the Mock Title at last, with a deep sigh, 'I was a real Title.'`;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
FooterComponent,
NavbarComponent,
],
imports: [
HttpClientTestingModule,
MatIconModule,
MatMenuModule,
RouterTestingModule,
],
providers: [
HttpClient,
FakeMatIconRegistry,
ApiService,
{provide: 'APP_ENVIRONMENT', useValue: mockEnvironment},
{provide: APP_BASE_HREF, useValue: '/'},
]
}).compileComponents();
}));
beforeEach(() => {
mockEnvironment.title = mockTitle;
fixture = TestBed.createComponent(AppComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the app', () => {
expect(component).toBeTruthy();
});
it(`should set the page title to match environment variable`, () => {
expect((component as any).titleService.getTitle()).toEqual(mockTitle);
});
});

27
src/app/app.component.ts Normal file
View File

@ -0,0 +1,27 @@
import {Component, Inject} from '@angular/core';
import {MatIconRegistry} from '@angular/material/icon';
import {DomSanitizer, Title} from '@angular/platform-browser';
import {Router} from '@angular/router';
import {AppEnvironment} from './interfaces/appEnvironment.interface';
import {GoogleAnalyticsService} from './services/google-analytics.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
loading: boolean;
constructor(
@Inject('APP_ENVIRONMENT') private environment: AppEnvironment,
private titleService: Title,
) {
this.titleService.setTitle(this.environment.title);
}
reload() {
this.loading = true;
setTimeout(() => this.loading = false, 300);
}
}

62
src/app/app.module.ts Normal file
View File

@ -0,0 +1,62 @@
import {APP_BASE_HREF, PlatformLocation} from '@angular/common';
import {HttpClientModule} from '@angular/common/http';
import {NgModule} from '@angular/core';
import {FlexLayoutModule} from '@angular/flex-layout';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MAT_FORM_FIELD_DEFAULT_OPTIONS} from '@angular/material/form-field';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {FormlyModule} from '@ngx-formly/core';
import {ThisEnvironment} from '../environments/environment.injectable';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {FooterComponent} from './footer/footer.component';
import {HomeComponent} from './home/home.component';
import {LoadingComponent} from './loading/loading.component';
import {NavbarComponent} from './navbar/navbar.component';
import {ApiService} from './services/api.service';
/**
* This function is used internal to get a string instance of the `<base href="" />` value from `index.html`.
* This is an exported function, instead of a private function or inline lambda, to prevent this error:
*
* `Error encountered resolving symbol values statically.`
* `Function calls are not supported.`
* `Consider replacing the function or lambda with a reference to an exported function.`
*
* @param platformLocation an Angular service used to interact with a browser's URL
* @return a string instance of the `<base href="" />` value from `index.html`
*/
export function getBaseHref(platformLocation: PlatformLocation): string {
return platformLocation.getBaseHrefFromDOM();
}
@NgModule({
declarations: [
AppComponent,
LoadingComponent,
FooterComponent,
NavbarComponent,
HomeComponent,
],
imports: [
BrowserAnimationsModule,
FlexLayoutModule,
FormlyModule,
FormsModule,
HttpClientModule,
MatProgressSpinnerModule,
ReactiveFormsModule,
AppRoutingModule // <-- This line MUST be last (https://angular.io/guide/router#module-import-order-matters)
],
providers: [
{provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'outline'}},
ApiService,
{provide: 'APP_ENVIRONMENT', useClass: ThisEnvironment},
{provide: APP_BASE_HREF, useFactory: getBaseHref, deps: [PlatformLocation]},
],
bootstrap: [AppComponent],
entryComponents: []
})
export class AppModule {
}

View File

@ -0,0 +1 @@
<p>footer works!</p>

View File

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FooterComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss']
})
export class FooterComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1 @@
<p>home works!</p>

View File

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HomeComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,5 @@
export interface ApiError {
status_code: number;
code: string;
message: string;
}

View File

@ -0,0 +1,6 @@
export interface AppEnvironment {
production: boolean;
api: string;
title: string;
googleAnalyticsKey: string;
}

View File

@ -0,0 +1,6 @@
export interface Sample {
id: string;
barCodeId: string;
locationId: string;
createdAd: string;
}

View File

@ -0,0 +1,5 @@
<div *ngIf="showSpinner" class="loading" fxLayoutAlign="center center">
{{message || ''}}
<mat-spinner [diameter]="diameter"></mat-spinner>
</div>
<span *ngIf="!showSpinner">{{message || '...'}}</span>

View File

View File

@ -0,0 +1,29 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import { LoadingComponent } from './loading.component';
describe('LoadingComponent', () => {
let component: LoadingComponent;
let fixture: ComponentFixture<LoadingComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LoadingComponent ],
imports: [
MatProgressSpinnerModule
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoadingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,34 @@
import {Component, Input, OnInit} from '@angular/core';
@Component({
selector: 'app-loading',
templateUrl: './loading.component.html',
styleUrls: ['./loading.component.scss']
})
export class LoadingComponent implements OnInit {
@Input() showSpinner = true;
@Input() message: string;
@Input() size = 'lg';
@Input() baseSize = 24;
get diameter(): number {
switch (this.size) {
case 'xl':
return this.baseSize * 4;
case 'lg':
return this.baseSize * 3;
case 'med':
return this.baseSize * 2;
case 'sm':
return this.baseSize;
default:
return this.baseSize * 3;
}
}
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1 @@
<p>navbar works!</p>

View File

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NavbarComponent } from './navbar.component';
describe('NavbarComponent', () => {
let component: NavbarComponent;
let fixture: ComponentFixture<NavbarComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ NavbarComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NavbarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-navbar',
templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.scss']
})
export class NavbarComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -0,0 +1,48 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {TestBed} from '@angular/core/testing';
import {MatBottomSheetModule} from '@angular/material/bottom-sheet';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {MockEnvironment} from '../testing/environment.mock';
import {ApiService} from './api.service';
describe('ApiService', () => {
let httpMock: HttpTestingController;
let location: Location;
let service: ApiService;
const mockEnvironment = new MockEnvironment();
const mockRouter = {
createUrlTree: jasmine.createSpy('createUrlTree'),
navigate: jasmine.createSpy('navigate')
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
MatBottomSheetModule,
RouterTestingModule.withRoutes([]),
],
providers: [
ApiService,
{provide: 'APP_ENVIRONMENT', useValue: mockEnvironment},
{provide: APP_BASE_HREF, useValue: '/'},
{provide: Router, useValue: mockRouter},
{provide: Location, useValue: location},
]
});
httpMock = TestBed.inject(HttpTestingController);
service = TestBed.inject(ApiService);
location = TestBed.inject(Location);
});
afterEach(() => {
httpMock.verify();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClient} from '@angular/common/http';
import {Inject, Injectable} from '@angular/core';
import {Router} from '@angular/router';
import {Observable, throwError} from 'rxjs';
import {catchError} from 'rxjs/operators';
import {ApiError} from '../interfaces/apiError.interface';
import {AppEnvironment} from '../interfaces/appEnvironment.interface';
import {Sample} from '../interfaces/sample.interface';
@Injectable({
providedIn: 'root'
})
export class ApiService {
apiRoot: string;
constructor(
@Inject('APP_ENVIRONMENT') private environment: AppEnvironment,
@Inject(APP_BASE_HREF) public baseHref: string,
private httpClient: HttpClient,
private router: Router,
private location: Location,
) {
this.apiRoot = environment.api;
}
/** Get the string value from a given URL */
getStringFromUrl(url: string): Observable<string> {
return this.httpClient
.get(url, {responseType: 'text'})
.pipe(catchError(err => this._handleError(err)));
}
/** Add new sample */
addSample(sample: Sample): Observable<Sample> {
const url = this.apiRoot;
return this.httpClient
.put<Sample>(url, sample)
.pipe(catchError(err => this._handleError(err)));
}
private _handleError(error: ApiError): Observable<never> {
return throwError(error.message || 'Could not complete your request; please try again later.');
}
}

View File

@ -0,0 +1,52 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpClientTestingModule} from '@angular/common/http/testing';
import {TestBed} from '@angular/core/testing';
import {Router} from '@angular/router';
import {RouterTestingModule} from '@angular/router/testing';
import {SessionRedirectComponent} from '../components/session-redirect/session-redirect.component';
import {MockEnvironment} from '../testing/mocks/environment.mocks';
import {GoogleAnalyticsService} from './google-analytics.service';
describe('GoogleAnalyticsService', () => {
let service: GoogleAnalyticsService;
const mockEnvironment = new MockEnvironment();
const mockRouter = {
createUrlTree: jasmine.createSpy('createUrlTree'),
navigate: jasmine.createSpy('navigate'),
events: jasmine.createSpyObj('events', ['subscribe']),
};
beforeEach(() => TestBed.configureTestingModule({
declarations: [SessionRedirectComponent],
imports: [
HttpClientTestingModule,
RouterTestingModule.withRoutes([
{
path: 'session/:token',
component: SessionRedirectComponent
}
])
],
providers: [
GoogleAnalyticsService,
{provide: 'APP_ENVIRONMENT', useValue: mockEnvironment},
{provide: APP_BASE_HREF, useValue: '/'},
{provide: Router, useValue: mockRouter},
{provide: Location, useValue: location},
],
}));
beforeEach(() => {
service = TestBed.inject(GoogleAnalyticsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
expect(service.analyticsKey).toBeTruthy();
});
it('should set a new analytics key', () => {
service.init('new_key');
expect(service.analyticsKey).toEqual('new_key');
});
});

View File

@ -0,0 +1,82 @@
import {APP_BASE_HREF} from '@angular/common';
import {HttpRequest} from '@angular/common/http';
import {Inject, Injectable} from '@angular/core';
import {NavigationEnd, Router} from '@angular/router';
import {ApiError} from '../interfaces/apiError.interface';
import {AppEnvironment} from '../interfaces/appEnvironment.interface';
declare var gtag;
@Injectable({
providedIn: 'root'
})
export class GoogleAnalyticsService {
analyticsKey: string;
constructor(
@Inject('APP_ENVIRONMENT') private environment: AppEnvironment,
@Inject(APP_BASE_HREF) public baseHref: string,
private router: Router,
) {
this.analyticsKey = this.environment.googleAnalyticsKey;
}
public authEvent(req: HttpRequest<any>) {
this.event('login', 'authentication', req.url)
}
public errorEvent(error: ApiError) {
this.event(error.code, 'error_messages', error.message);
}
public setUser(uid) {
if (gtag) {
gtag('set', {user_id: uid}); // Set the user ID using signed-in user_id.
this.event('user-id available', 'authentication', uid)
}
}
public init(analyticsKey) {
this.analyticsKey = analyticsKey || this.analyticsKey;
this.listenForRouteChanges();
try {
const script1 = document.createElement('script');
script1.async = true;
script1.src = 'https://www.googletagmanager.com/gtag/js?id=' + this.analyticsKey;
document.head.appendChild(script1);
const script2 = document.createElement('script');
script2.innerHTML = `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '` + this.analyticsKey + `', {'send_page_view': false});
`;
document.head.appendChild(script2);
} catch (ex) {
console.error('Error appending google analytics');
console.error(ex);
}
}
private event(action: string, category: string, label: string) {
if (gtag) {
gtag('event', action, {
event_category: category,
event_label: label
});
}
}
private listenForRouteChanges() {
const analyticsKey = this.environment.googleAnalyticsKey;
this.router.events.subscribe(event => {
if (gtag && event instanceof NavigationEnd) {
gtag('config', analyticsKey, {
page_path: event.urlAfterRedirects,
});
}
});
}
}

View File

@ -0,0 +1,10 @@
import {Injectable} from '@angular/core';
import {AppEnvironment} from '../interfaces/appEnvironment.interface';
@Injectable()
export class MockEnvironment implements AppEnvironment {
production = false;
api = 'apiRoot';
title = 'Mock Title';
googleAnalyticsKey = '';
}

0
src/assets/.gitkeep Normal file
View File

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M9,16A2,2 0 0,0 7,18A2,2 0 0,0 9,20A2,2 0 0,0 11,18V13H14V11H10V16.27C9.71,16.1 9.36,16 9,16Z" /></svg>

After

Width:  |  Height:  |  Size: 480 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M17,19V13L14,15.2V13H7V19H14V16.8L17,19Z" /></svg>

After

Width:  |  Height:  |  Size: 427 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M17,11H13V13H14L12,14.67L10,13H11V11H7V13H8L11,15.5L8,18H7V20H11V18H10L12,16.33L14,18H13V20H17V18H16L13,15.5L16,13H17V11Z" /></svg>

After

Width:  |  Height:  |  Size: 501 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>

After

Width:  |  Height:  |  Size: 754 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M13,3.5L18.5,9H13V3.5M12,11A3,3 0 0,1 15,14C15,15.88 12.75,16.06 12.75,17.75H11.25C11.25,15.31 13.5,15.5 13.5,14A1.5,1.5 0 0,0 12,12.5A1.5,1.5 0 0,0 10.5,14H9A3,3 0 0,1 12,11M11.25,18.5H12.75V20H11.25V18.5Z" /></svg>

After

Width:  |  Height:  |  Size: 569 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>

After

Width:  |  Height:  |  Size: 754 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M17,19V13L14,15.2V13H7V19H14V16.8L17,19Z" /></svg>

After

Width:  |  Height:  |  Size: 427 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M9,16A2,2 0 0,0 7,18A2,2 0 0,0 9,20A2,2 0 0,0 11,18V13H14V11H10V16.27C9.71,16.1 9.36,16 9,16Z" /></svg>

After

Width:  |  Height:  |  Size: 480 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M17,19V13L14,15.2V13H7V19H14V16.8L17,19Z" /></svg>

After

Width:  |  Height:  |  Size: 427 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M17,19V13L14,15.2V13H7V19H14V16.8L17,19Z" /></svg>

After

Width:  |  Height:  |  Size: 427 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>

After

Width:  |  Height:  |  Size: 754 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M9,16A2,2 0 0,0 7,18A2,2 0 0,0 9,20A2,2 0 0,0 11,18V13H14V11H10V16.27C9.71,16.1 9.36,16 9,16Z" /></svg>

After

Width:  |  Height:  |  Size: 480 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M14,9H19.5L14,3.5V9M7,2H15L21,8V20A2,2 0 0,1 19,22H7C5.89,22 5,21.1 5,20V4A2,2 0 0,1 7,2M11.93,12.44C12.34,13.34 12.86,14.08 13.46,14.59L13.87,14.91C13,15.07 11.8,15.35 10.53,15.84V15.84L10.42,15.88L10.92,14.84C11.37,13.97 11.7,13.18 11.93,12.44M18.41,16.25C18.59,16.07 18.68,15.84 18.69,15.59C18.72,15.39 18.67,15.2 18.57,15.04C18.28,14.57 17.53,14.35 16.29,14.35L15,14.42L14.13,13.84C13.5,13.32 12.93,12.41 12.53,11.28L12.57,11.14C12.9,9.81 13.21,8.2 12.55,7.54C12.39,7.38 12.17,7.3 11.94,7.3H11.7C11.33,7.3 11,7.69 10.91,8.07C10.54,9.4 10.76,10.13 11.13,11.34V11.35C10.88,12.23 10.56,13.25 10.05,14.28L9.09,16.08L8.2,16.57C7,17.32 6.43,18.16 6.32,18.69C6.28,18.88 6.3,19.05 6.37,19.23L6.4,19.28L6.88,19.59L7.32,19.7C8.13,19.7 9.05,18.75 10.29,16.63L10.47,16.56C11.5,16.23 12.78,16 14.5,15.81C15.53,16.32 16.74,16.55 17.5,16.55C17.94,16.55 18.24,16.44 18.41,16.25M18,15.54L18.09,15.65C18.08,15.75 18.05,15.76 18,15.78H17.96L17.77,15.8C17.31,15.8 16.6,15.61 15.87,15.29C15.96,15.19 16,15.19 16.1,15.19C17.5,15.19 17.9,15.44 18,15.54M8.83,17C8.18,18.19 7.59,18.85 7.14,19C7.19,18.62 7.64,17.96 8.35,17.31L8.83,17M11.85,10.09C11.62,9.19 11.61,8.46 11.78,8.04L11.85,7.92L12,7.97C12.17,8.21 12.19,8.53 12.09,9.07L12.06,9.23L11.9,10.05L11.85,10.09Z" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>

After

Width:  |  Height:  |  Size: 754 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M8,11V13H9V19H8V20H12V19H11V17H13A3,3 0 0,0 16,14A3,3 0 0,0 13,11H8M13,13A1,1 0 0,1 14,14A1,1 0 0,1 13,15H11V13H13Z" /></svg>

After

Width:  |  Height:  |  Size: 495 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M8,11V13H9V19H8V20H12V19H11V17H13A3,3 0 0,0 16,14A3,3 0 0,0 13,11H8M13,13A1,1 0 0,1 14,14A1,1 0 0,1 13,15H11V13H13Z" /></svg>

After

Width:  |  Height:  |  Size: 495 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z" /></svg>

After

Width:  |  Height:  |  Size: 545 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H18A2,2 0 0,1 20,4V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M12,4A6,6 0 0,0 6,10C6,13.31 8.69,16 12.1,16L11.22,13.77C10.95,13.29 11.11,12.68 11.59,12.4L12.45,11.9C12.93,11.63 13.54,11.79 13.82,12.27L15.74,14.69C17.12,13.59 18,11.9 18,10A6,6 0 0,0 12,4M12,9A1,1 0 0,1 13,10A1,1 0 0,1 12,11A1,1 0 0,1 11,10A1,1 0 0,1 12,9M7,18A1,1 0 0,0 6,19A1,1 0 0,0 7,20A1,1 0 0,0 8,19A1,1 0 0,0 7,18M12.09,13.27L14.58,19.58L17.17,18.08L12.95,12.77L12.09,13.27Z" /></svg>

After

Width:  |  Height:  |  Size: 754 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z" /></svg>

After

Width:  |  Height:  |  Size: 486 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2H14L20,8V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V4A2,2 0 0,1 6,2M13,3.5V9H18.5L13,3.5M7,13L8.5,20H10.5L12,17L13.5,20H15.5L17,13H18V11H14V13H15L14.1,17.2L13,15V15H11V15L9.9,17.2L9,13H10V11H6V13H7Z" /></svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M13,3.5L18.5,9H13V3.5M12,11A3,3 0 0,1 15,14C15,15.88 12.75,16.06 12.75,17.75H11.25C11.25,15.31 13.5,15.5 13.5,14A1.5,1.5 0 0,0 12,12.5A1.5,1.5 0 0,0 10.5,14H9A3,3 0 0,1 12,11M11.25,18.5H12.75V20H11.25V18.5Z" /></svg>

After

Width:  |  Height:  |  Size: 569 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M9,16A2,2 0 0,0 7,18A2,2 0 0,0 9,20A2,2 0 0,0 11,18V13H14V11H10V16.27C9.71,16.1 9.36,16 9,16Z" /></svg>

After

Width:  |  Height:  |  Size: 480 B

Some files were not shown because too many files have changed in this diff Show More