247 Commits

Author SHA1 Message Date
Jeff Avallone 54b48f5581 Improving test coverage 2018-06-08 21:05:35 -04:00
Jeff Avallone 52d378be61 Removing jest-junit
Not using CircleCI anymore, so don't need it
2018-06-08 15:10:51 -04:00
Jeff Avallone f7c803a3a6 Removing need for getDerivedStateFromProps 2018-06-07 22:13:15 -04:00
Jeff Avallone d3a0746ebb Removing unnecessary constructor 2018-06-07 21:53:15 -04:00
Jeff Avallone 03ef9b2203 Removing badges from README 2018-06-06 16:41:07 -04:00
Jeff Avallone 6c934bcb0f Fixing invalidation ID for deployments 2018-06-06 01:33:31 -04:00
Jeff Avallone 10196afec5 Updating CI config to build correctly 2018-06-05 16:37:43 -04:00
Jeff Avallone 79fa7d073d Setting image to use for builds 2018-06-05 08:41:15 -04:00
Jeff Avallone 04cec072e3 Tagging pipeline steps to use shared runners 2018-06-05 07:18:41 -04:00
Jeff Avallone a0468fd06f Adding GitLab CI config 2018-06-05 07:09:06 -04:00
Jeff Avallone 99b28eb5f2 Adding GitLab CI status badges 2018-06-05 06:40:21 -04:00
Jeff Avallone 472273e7e2 Migrating to GitLab (CI config TBD) 2018-06-04 21:23:27 -04:00
Jeff Avallone 76785dcfa8 Package upgrades 2018-06-02 13:35:58 -04:00
Jeff Avallone 14eeab2098 Upgrading Jest 2018-06-02 13:13:37 -04:00
Jeff Avallone fd656c8649 Upgrading React and Form's getDerivedStateFromProps 2018-06-02 13:01:16 -04:00
Jeff Avallone 7a8e90d3ca Style tweaks 2018-06-02 06:43:03 -04:00
Jeff Avallone 919612bc0d Fixing translation behavior of download link labels 2018-06-01 17:59:24 -04:00
Jeff Avallone cda91cd3f5 Adding "English (ALL-CAPS)" as a language
This is to provide a sample language other than English. Since I don't
speak any other languages with anything even approaching enough fluency
to do a translation, this joke of a "translation" will have to do.
2018-06-01 17:50:55 -04:00
Jeff Avallone a3de3e522f I should check the file extension used before commiting the docs 2018-06-01 17:41:54 -04:00
Jeff Avallone 60e524c134 Fixing wording in the Privacy Policy 2018-06-01 17:37:45 -04:00
Jeff Avallone 295739b079 Adding docs for updating translatoins 2018-06-01 17:36:32 -04:00
Jeff Avallone 6b8a92df8b Making language switcher a styled select list 2018-06-01 17:19:52 -04:00
Jeff Avallone 12436fbd9e Extracting select list styles to be re-used 2018-06-01 17:19:27 -04:00
Jeff Avallone 9ff0a51006 Moving Privacy Policy link to the header 2018-06-01 17:18:23 -04:00
Jeff Avallone 81aceeba5f Modifying separated inline list styles to wrap neatly
The "//" separator is hidden when the list items wrap.
2018-06-01 17:16:54 -04:00
Jeff Avallone 8eb2398de1 Adjusting browser list and cssnext configs 2018-06-01 17:12:35 -04:00
Jeff Avallone befa23b4ea Adding missing translation 2018-06-01 17:11:48 -04:00
Jeff Avallone 2656edf27e Adding LocaleSwitcher to Header 2018-05-31 19:07:29 -04:00
Jeff Avallone 2fc961725a Updating Header styles to allow right-hand items to reflow 2018-05-31 19:07:11 -04:00
Jeff Avallone efc8f6744a Adding a LocaleSwitch component
Still needs some styling, but it is functional
2018-05-31 19:06:33 -04:00
Jeff Avallone 479d035cd0 Updating i18n:scrub script to remove defined keys from missing namespace 2018-05-31 18:23:04 -04:00
Jeff Avallone b575b9a0cc Adding check to deploy script for file to delete before attempting 2018-05-31 17:26:32 -04:00
Jeff Avallone 1eb3ca9182 Fixing typo 2018-05-31 17:17:23 -04:00
Jeff Avallone a2b9808613 Adding PWA install prompt 2018-05-29 19:01:19 -04:00
Jeff Avallone 589b1aa9e0 Tweaking test summary setup 2018-05-29 17:36:29 -04:00
Jeff Avallone 49e56aebd7 Setting up CircleCI test summary support 2018-05-29 17:30:51 -04:00
Jeff Avallone 48903eb54e Using the AWS SDK correctly 2018-05-29 17:13:38 -04:00
Jeff Avallone 43160ca71e Fixing un-updated config value name 2018-05-29 17:11:20 -04:00
Jeff Avallone 16e900e5df Invalidating CloudFront cache when deploying 2018-05-29 17:08:22 -04:00
Jeff Avallone 41676a370f Removing sample messages from App 2018-05-28 16:08:05 -04:00
Jeff Avallone e71f1b4cc2 Cleaning up console.log calls in tests 2018-05-28 14:48:10 -04:00
Jeff Avallone 8ba954c743 Loading rendering code dynamically 2018-05-28 14:41:02 -04:00
Jeff Avallone d88defcc65 Moving logic to generate SVG and PNG blobs to SVG/Image
This is to avoid reaching into the Image component to access the SVG ref
2018-05-28 12:47:49 -04:00
Jeff Avallone 3916d63f2d Switching to use forwardRef 2018-05-28 12:28:08 -04:00
Jeff Avallone b9f6766a66 Using React.createRef 2018-05-28 12:16:44 -04:00
Jeff Avallone 41f10c0dc3 Removing syntax map from devel.js 2018-05-28 12:10:15 -04:00
Jeff Avallone 9a10fc35e8 Adjusting chunk splitting config 2018-05-28 12:03:49 -04:00
Jeff Avallone 2a4e195742 Removing unnecessary config 2018-05-28 11:21:25 -04:00
Jeff Avallone 6ced6fddd5 Renaming CircleCI contexts to be more specific 2018-05-28 11:12:52 -04:00
Jeff Avallone ab786ae242 Updating build status badge 2018-05-28 11:02:35 -04:00
Jeff Avallone a22e858648 Adding documentation about configuration via environment variables 2018-05-28 11:02:13 -04:00
Jeff Avallone 7aa95e092b Updatnig README scripts section 2018-05-28 10:38:35 -04:00
Jeff Avallone 537bf3eb11 Renaming deploy config file 2018-05-28 10:33:51 -04:00
Jeff Avallone c7a7acc1c9 Fixing incorrect argument 2018-05-27 13:49:55 -04:00
Jeff Avallone 25197b9abd Adding more flexible deploy script 2018-05-27 13:45:26 -04:00
Jeff Avallone 4685337599 Adjusting styles 2018-05-25 15:21:31 -04:00
Jeff Avallone ced39a279f Typo 2018-05-25 14:54:11 -04:00
Jeff Avallone f2d10483ad Fixing content-type for service-worker in S3 2018-05-25 14:49:07 -04:00
Jeff Avallone 92800241d7 Setting cache-control header for service-worker.js file 2018-05-25 14:37:25 -04:00
Jeff Avallone beb73f3e69 Adjusting service worker config 2018-05-25 14:37:07 -04:00
Jeff Avallone 8f50e2e232 Turning off cache for local prod server 2018-05-25 14:36:33 -04:00
Jeff Avallone cb0c39b081 Whitespace 2018-05-25 14:36:16 -04:00
Jeff Avallone 133e0c9ec3 Adding build ID to Footer component 2018-05-25 14:35:32 -04:00
Jeff Avallone d63d2484da Adding some logging when Do Not Track disables stuff 2018-05-24 18:04:57 -04:00
Jeff Avallone 99ecc123f4 Refering to the privacy policy from JS Disabled error message 2018-05-24 17:57:20 -04:00
Jeff Avallone 54a45ea3be Linking privacy policy in the footer 2018-05-24 17:56:35 -04:00
Jeff Avallone 821c2eb7fe Adding privacy policy page 2018-05-24 17:56:35 -04:00
Jeff Avallone 031dc2235a Adding support for "info"-type Messages 2018-05-24 17:56:35 -04:00
Jeff Avallone 1a8bb762cb Supporting "Do Not Track" for Gogle Analytics and Sentry error reporting 2018-05-24 17:01:17 -04:00
Jeff Avallone 7698901451 Fixing service worker URL 2018-05-16 18:47:02 -04:00
Jeff Avallone f3a2acdd7f Updating dependencies 2018-05-16 18:46:58 -04:00
Jeff Avallone 0ac27391c7 Updating packages 2018-04-08 22:07:52 -04:00
Jeff Avallone f0062c94be Switching to use React.createRef 2018-03-31 11:44:26 -04:00
Jeff Avallone f5f30a854b Ensuring react-test-renderer is the correct version and enabling test 2018-03-31 05:18:54 -04:00
Jeff Avallone 1b87abb4fe Updating chunk splitting config 2018-03-30 17:06:33 -04:00
Jeff Avallone 0b76e5979c Adding webpack bundle analyzer 2018-03-30 17:06:11 -04:00
Jeff Avallone a471a33014 Splitting prod configs 2018-03-30 17:05:33 -04:00
Jeff Avallone 5c2b06d4e9 Upgrading i18next packages 2018-03-29 21:34:25 -04:00
Jeff Avallone 2d19ae09bf Using a path in favicon SVG since font wasn't loading 2018-03-29 21:31:48 -04:00
Jeff Avallone 8ef5a5483d Upgrading Workbox plugin 2018-03-29 21:25:45 -04:00
Jeff Avallone a3ad7d0e08 Upgrading webpack packages 2018-03-29 21:10:21 -04:00
Jeff Avallone 50a2339cc1 Updating more packages 2018-03-29 20:16:21 -04:00
Jeff Avallone cd6ccf5838 Upgrading eslint packages 2018-03-29 20:10:38 -04:00
Jeff Avallone 0234371309 Upgrading Jest and related dependencies 2018-03-29 19:05:23 -04:00
Jeff Avallone fe7caf5faa Updating React and switching to new lifecycle methods 2018-03-29 19:00:52 -04:00
Jeff Avallone 91b6ac8668 Upgrading other packages 2018-03-07 19:09:27 -05:00
Jeff Avallone cbe538c007 Upgrading webpack to 4.0
Needed to replace favicon generator
2018-03-07 19:09:08 -05:00
Jeff Avallone e28fd69c73 Tweaking cache regexes 2018-02-25 17:33:55 -05:00
Jeff Avallone 9c9d3141e9 Removing unnecessary code 2018-02-25 11:49:29 -05:00
Jeff Avallone 18bd368525 Removing return from reflow implementations
I don't think it will be necessary to wait for components to complete
re-rendering before moving on with the reflow process. Parent components
only depend on the bounding box and that is determined immediately when
reflow is called
2018-02-25 11:47:57 -05:00
Jeff Avallone 1e8ee71aef Updating dependencies 2018-02-24 17:07:22 -05:00
Jeff Avallone e5e6f1d0c8 Adding font and lone external image to the service worker cache 2018-02-24 16:43:08 -05:00
Jeff Avallone 692b9fa2df Cleaning up tests 2018-02-20 21:00:48 -05:00
Jeff Avallone f90b4a7bed Adding tests for updating the expression and syntax 2018-02-20 21:00:48 -05:00
Jeff Avallone 33a473734b Updating tests to cover an edge case 2018-02-20 20:25:06 -05:00
Jeff Avallone aa278fb193 Adding URL handling 2018-02-19 17:29:08 -05:00
Jeff Avallone 50d05c423d Using modules in babel
Switching back to running tests in production environment
2018-02-19 17:01:52 -05:00
Jeff Avallone 2dd2132a2b Revert "Running tests in production environment to remove spurious warnings"
This reverts commit 0a9b0f6bfb.
2018-02-18 16:26:36 -05:00
Jeff Avallone 0a9b0f6bfb Running tests in production environment to remove spurious warnings
This gets rid of the warnings from React due to jsdom not recognizing
SVG elements. Looks like there's a PR against jsdom that might fix this,
so once that's in this change can be reverted
2018-02-18 16:11:25 -05:00
Jeff Avallone ef33cdab04 Adding tests for Path#arcTo 2018-02-18 16:03:46 -05:00
Jeff Avallone d6cdad7ec3 Adding tests for Path#quadraticCurveTo 2018-02-18 16:01:24 -05:00
Jeff Avallone 354b65b623 Adding tests for cubicCurveTo 2018-02-18 15:58:23 -05:00
Jeff Avallone 9716e166df Adding test for Path#closePath 2018-02-18 15:52:40 -05:00
Jeff Avallone 45b652d9af Adding tests for Path#moveTo 2018-02-18 15:49:20 -05:00
Jeff Avallone bb37848265 Adding specs and some refactoring for the Path#lineTo method 2018-02-18 15:49:03 -05:00
Jeff Avallone 381df8bf93 Adding tests for root SVG component 2018-02-18 11:49:09 -05:00
Jeff Avallone 6285bd4320 Upgrading packages 2018-02-17 22:26:24 -05:00
Jeff Avallone cf824ac334 Adjusting text color to improve contrast 2018-02-17 22:19:39 -05:00
Jeff Avallone a1ef89b6fb Refining header styles 2018-02-17 22:07:49 -05:00
Jeff Avallone 9eea045d2b Fixing centering of rendered SVG when container is too small 2018-02-17 17:25:54 -05:00
Jeff Avallone 1362ceb8c8 Adding slight text-shadow to header to get logo popping a little 2018-02-17 17:21:18 -05:00
Jeff Avallone 6e8d7c297a Moving to using a decorator mixin instead of base class for SVGs 2018-02-17 16:58:49 -05:00
Jeff Avallone 44e6dae289 Simplifying "pre-reflow" logic 2018-02-17 16:28:46 -05:00
Jeff Avallone 1f9ba28099 Removing an unused property 2018-02-17 16:27:18 -05:00
Jeff Avallone 6c4972e726 Updating how initial state is set 2018-02-17 16:14:59 -05:00
Jeff Avallone 13dc496a02 Some optimization of immutable object use 2018-02-17 16:13:19 -05:00
Jeff Avallone c7aca59afc Tweaking ExtractTextPlugin config 2018-02-17 15:36:15 -05:00
Jeff Avallone d2651c585f Turn off modules in babel build (webpack handles it) 2018-02-17 15:20:23 -05:00
Jeff Avallone 6ff9145603 Adding tests for SVG components
Jest/enzyme/jsdom is kicking out some nastly looking messages because it
doesn't recognize various SVG elements, but they appear to be harmless.
2018-02-17 13:04:19 -05:00
Jeff Avallone 19d34a4d9e Adding extraneous PropType 2018-02-17 12:50:27 -05:00
Jeff Avallone f364673388 Adding promisified setState and simplfying reflow code for SVG stuff 2018-02-17 12:06:35 -05:00
Jeff Avallone e04e4edc1f Renaming type property to theme 2018-02-17 11:23:20 -05:00
Jeff Avallone 3ead0c13df Refactoring rendering root to be a component 2018-02-17 11:16:05 -05:00
Jeff Avallone dea6d92272 Reorganizing the rendering flow 2018-02-17 10:45:03 -05:00
Jeff Avallone 2a0e0149fd Toning down borders on literal and escape boxes 2018-02-17 10:26:01 -05:00
Jeff Avallone c047dab5a4 Adding style for anchor elements 2018-02-17 06:24:50 -05:00
Jeff Avallone 06a90429ff Getting Download links working 2018-02-16 22:47:53 -05:00
Jeff Avallone 6bc4306ca3 Fleshing out expample render and cleaning up some SVG code 2018-02-16 21:48:01 -05:00
Jeff Avallone 82b780e9c3 First cut of SVG rendering components
These still need work, but they're functional enough to render a diagram
2018-02-16 19:16:30 -05:00
Jeff Avallone 3fdc74bdf2 Adding color output to prerender script and relocating to script 2018-02-16 17:16:23 -05:00
Jeff Avallone d69ab00ad5 Updating i18n:scrub command with better output 2018-02-16 17:15:45 -05:00
Jeff Avallone 8e8fbd3219 Making GitHub and CC license links open in new window 2018-02-16 17:14:29 -05:00
Jeff Avallone 0d41100e04 Updating formatting 2018-02-15 21:21:43 -05:00
Jeff Avallone 5218906385 Adding script to scrub locale files
For any keys in en/translation.yaml and missing in
<lang>/translation.yaml, this tool will add the key to
<lang>/missing.yaml. This is to facilitate translation of keys that have
been added to the app.

This also serves as a lint-like tool for translation files since they
will all be re-written when running.
2018-02-15 21:19:25 -05:00
Jeff Avallone adba2999bf Updating prerender script to use async/await 2018-02-15 20:26:03 -05:00
Jeff Avallone 17e8be5f42 Updating packages 2018-02-15 20:03:10 -05:00
Jeff Avallone 9adaa6041d Formatting 2018-02-15 20:03:04 -05:00
Jeff Avallone 8a18304225 Tweaking design of error and warning messages 2018-02-15 19:46:11 -05:00
Jeff Avallone 521f7965b0 Making border in select less pronounced 2018-02-15 19:32:25 -05:00
Jeff Avallone 74f1513311 Moving configs into package.json 2018-02-15 18:17:40 -05:00
Jeff Avallone d634985698 Setting environment reported to Sentry separate from NODE_ENV
For preview and prod, NODE_ENV is "production"
2018-02-15 17:55:37 -05:00
Jeff Avallone f3e3e7922c Adding line next to select arrow 2018-02-15 17:52:04 -05:00
Jeff Avallone 569c06b041 Softening header shadow and adding shadows to messages 2018-02-15 17:48:30 -05:00
Jeff Avallone abe646cec8 Adding tests for page components 2018-02-15 17:35:59 -05:00
Jeff Avallone 8b2ce32b75 Fleshing out Form tests 2018-02-15 17:35:45 -05:00
Jeff Avallone 7a8a9836aa Adding to i18n mocks 2018-02-15 17:35:12 -05:00
Jeff Avallone 79191c0fd7 Improving coverage in RavenError tests 2018-02-15 17:33:43 -05:00
Jeff Avallone d9af19ca63 Cleanup setup modules 2018-02-15 17:33:14 -05:00
Jeff Avallone fcba4b75ec Moving Jest setup and consolidating 2018-02-15 17:27:15 -05:00
Jeff Avallone 2904519ff5 Tweaking for styles 2018-02-15 17:24:55 -05:00
Jeff Avallone 927718832b Cleaning up back copy-paste job 2018-02-15 17:23:15 -05:00
Jeff Avallone eb384831fe Tweaking Jest config 2018-02-15 17:22:25 -05:00
Jeff Avallone 895827e881 Moving list styles complete to use @apply 2018-02-15 09:08:50 -05:00
Jeff Avallone 672ded87af Fixing positoining of action links in Form 2018-02-15 09:05:07 -05:00
Jeff Avallone bec4279c31 Cleanup tests 2018-02-13 21:38:10 -05:00
Jeff Avallone 6cf064eaf0 Fleshing out messaging 2018-02-13 21:36:10 -05:00
Jeff Avallone d328727ceb Adding support for default icons based on Message type 2018-02-13 21:28:31 -05:00
Jeff Avallone b83f5cd34d Adding Form component 2018-02-13 21:23:49 -05:00
Jeff Avallone 597cce4566 Extracting green gradient 2018-02-13 21:23:33 -05:00
Jeff Avallone 364139d362 Extracting translation mock 2018-02-13 21:23:11 -05:00
Jeff Avallone 3931cee8af Updating import paths in tests 2018-02-13 20:18:26 -05:00
Jeff Avallone 91ee254477 Moving App into a directory and adding tests 2018-02-13 20:13:11 -05:00
Jeff Avallone 8817b5f027 Setting up webpack resolve root 2018-02-13 20:09:52 -05:00
Jeff Avallone 6b55f1ec72 CSS modules EVERYWHERE! 2018-02-13 18:15:18 -05:00
Jeff Avallone 6ab4978a03 Documenting scripts in README 2018-02-13 17:57:03 -05:00
Jeff Avallone d8a8177f41 Setting NODE_ENV for builds 2018-02-13 17:54:14 -05:00
Jeff Avallone 888336cbec Adding loader for i18next translation files 2018-02-13 17:46:44 -05:00
Jeff Avallone 5ab4a70414 Adjusting formatting on 404 page title and adding PropTypes 2018-02-13 17:34:22 -05:00
Jeff Avallone 94e511bb96 Further improvements to page prerendering 2018-02-13 17:28:47 -05:00
Jeff Avallone 44a9cad9b3 Removing unused PageTemplate component 2018-02-13 17:26:32 -05:00
Jeff Avallone 24062d978a Improving SVG mocking for tests 2018-02-13 17:17:55 -05:00
Jeff Avallone 1fd797f52a Adding identity-obj-proxy for CSS module mocks 2018-02-13 17:15:33 -05:00
Jeff Avallone 20adf55c11 Breaking up main stylesheet into per-component styles 2018-02-13 17:12:02 -05:00
Jeff Avallone 7238643740 Reworking static page generation
Including styles in components wasn't working with the old system.
2018-02-13 17:10:32 -05:00
Jeff Avallone ad6583d5dc Reorganizing components into directories 2018-02-12 20:05:47 -05:00
Jeff Avallone 21146549f7 Adding language detection 2018-02-11 18:43:11 -05:00
Jeff Avallone 355ef79d20 Adding i18next 2018-02-11 18:37:07 -05:00
Jeff Avallone 63766e84e9 Breaking Header and Footer out into components
For forthcoming i18n setup
2018-02-11 15:58:59 -05:00
Jeff Avallone 4923bbd985 Adding some styling to messages to spice up errors 2018-02-11 15:27:01 -05:00
Jeff Avallone 63e56c5df7 Setting browserslist 2018-02-11 14:56:39 -05:00
Jeff Avallone eda8daf8dd Switching to use cssnext 2018-02-11 14:51:17 -05:00
Jeff Avallone d4d764f81e Storing build and coverage report 2018-02-11 13:12:23 -05:00
Jeff Avallone ca41d2015a Adding test coverage 2018-02-11 13:10:24 -05:00
Jeff Avallone 152bf1e361 Updating noscript statement 2018-02-11 12:49:02 -05:00
Jeff Avallone c81943628c Fixing production sourcemaps 2018-02-11 12:37:41 -05:00
Jeff Avallone 4600d1748a Doing the short hash correctly 2018-02-11 12:18:29 -05:00
Jeff Avallone e0716ef683 Adding tests for RavenBoundary 2018-02-11 12:13:15 -05:00
Jeff Avallone edf4ecd081 Removing react-test-renderer 2018-02-11 11:58:03 -05:00
Jeff Avallone 7caf439c53 Adding tests for RavenError 2018-02-11 11:57:23 -05:00
Jeff Avallone 5f11a11ba2 Updating PageTemplate tests to use enzyme 2018-02-11 11:52:35 -05:00
Jeff Avallone 637c9c2afd Updating Message tests to use enzyme 2018-02-11 11:50:08 -05:00
Jeff Avallone cabc2adc1f Adding enzyme 2018-02-11 11:49:50 -05:00
Jeff Avallone 90e57e26e7 Updating PageTemplate tests 2018-02-11 11:25:15 -05:00
Jeff Avallone 6ec546ace1 Adding error boundary using Sentry.io 2018-02-11 11:18:27 -05:00
Jeff Avallone 8b86ddc14c Updating babel and eslint configs
Adding support for class properties amongst other things
2018-02-11 11:16:07 -05:00
Jeff Avallone 5afca2241a Fixing some propTypes 2018-02-11 10:57:41 -05:00
Jeff Avallone b009d078b6 Adding Sentry.io 2018-02-11 10:41:03 -05:00
Jeff Avallone 6bf094a4c1 Declaritively initializing service worker 2018-02-11 10:29:35 -05:00
Jeff Avallone fd5a8786d0 Adding Google Analytics 2018-02-11 10:27:43 -05:00
Jeff Avallone 214a9eb5c1 I hate typos 2018-02-11 10:08:23 -05:00
Jeff Avallone 1e07be44da Trying install with more output 2018-02-11 10:06:59 -05:00
Jeff Avallone 1e45520e67 Updating packages before attempting install 2018-02-11 09:59:54 -05:00
Jeff Avallone 6dcd0595ae More fixes to deploy scripting 2018-02-11 09:58:44 -05:00
Jeff Avallone 9abbb86f2c Fixing awscli installation for deploy 2018-02-11 09:57:10 -05:00
Jeff Avallone a30d1f07fc Mocking environment for PageTemplate tests 2018-02-11 09:45:46 -05:00
Jeff Avallone 70455ea2b8 Moving build-time deps out of devDependencies 2018-02-11 09:41:03 -05:00
Jeff Avallone 37af8c24c8 Adding deploy scripting 2018-02-11 09:22:37 -05:00
Jeff Avallone bbdacca1da Adding an environment banner to the page
To prevent testing local changes in production
2018-02-11 09:16:33 -05:00
Jeff Avallone 84df219d7f Creating separate templates for each page 2018-02-11 07:42:52 -05:00
Jeff Avallone 95a6709ec0 Pulling most of the page template into a component 2018-02-11 07:28:34 -05:00
Jeff Avallone bdf54945fe Keeping description consistent across app 2018-02-11 07:11:16 -05:00
Jeff Avallone eef5d50436 Tweaking some spacing 2018-02-11 07:09:03 -05:00
Jeff Avallone 69999fa948 Fixing postcss config 2018-02-11 07:04:36 -05:00
Jeff Avallone 49a236bf89 Switching back to SVG components for icons
No longer need the hack to only use them in components while URLs are
used in styles since I don't need icons in the styles anymore. Embedding
the markup also provides the option to restyle the icons
2018-02-11 06:58:59 -05:00
Jeff Avallone 1de5079aa9 Rendering page template using React
Allows for using components (like Message) when rendering the base page
template
2018-02-11 06:53:34 -05:00
Jeff Avallone 810b37aa47 Removing SVG components
Sticking with importing data URIs for now
2018-02-11 06:51:54 -05:00
Jeff Avallone c368e9031f Reworking Message component
* Supporting URLs for icon
* Moving styles into top-level stylesheet for use in the template
2018-02-11 06:41:15 -05:00
Jeff Avallone b3e4bd2cff Switching to using latest node image 2018-02-10 21:01:58 -05:00
Jeff Avallone 4e46aa76be Removing debug output 2018-02-10 20:58:59 -05:00
Jeff Avallone adb579ed8b Fixing job names 2018-02-10 20:57:52 -05:00
Jeff Avallone 189dfe29f2 Tweaking CI config
Trying out Contexts
2018-02-10 20:57:13 -05:00
Jeff Avallone a3d6717786 Adding start of preview/prod deployments in CircleCI config 2018-02-10 20:49:11 -05:00
Jeff Avallone 026cc1fcea Limiting CircleCI build to only react branch 2018-02-10 20:30:33 -05:00
Jeff Avallone 9f5bb8faf1 Trying out CircleCI 2018-02-10 20:25:00 -05:00
Jeff Avallone c38a9ddb3b Reworking test commands to include lint with unit tests
Also adding a test:watch command
2018-02-10 19:23:56 -05:00
Jeff Avallone 10125066d9 Fixing URLs in yarn.lock
Also adding a project yarnrc so I don't end up using my internal npm
repo again.
2018-02-10 19:15:35 -05:00
Jeff Avallone 5050291ab7 Initial tweaking to TravisCI script 2018-02-10 18:34:50 -05:00
Jeff Avallone ef4259908c Using gitignore for eslint 2018-02-10 18:31:16 -05:00
Jeff Avallone 3e729b2a34 Adding Jest 2018-02-10 18:30:07 -05:00
Jeff Avallone 916b38c6c5 Moving webpack-dev-server to dev dependencies 2018-02-10 18:14:22 -05:00
Jeff Avallone 738902d0ce Adding precommit hook 2018-02-10 18:11:30 -05:00
Jeff Avallone cf0c175d0a Addressing lint errors 2018-02-10 18:10:30 -05:00
Jeff Avallone cca35117f5 Adding eslint 2018-02-10 18:03:21 -05:00
Jeff Avallone 74d622c7a4 Getting icon loading in the root styles working 2018-02-10 17:55:48 -05:00
Jeff Avallone 2377cb2497 Removing unnecessary imports 2018-02-10 17:27:30 -05:00
Jeff Avallone 1576904f9c Ahem... 2018-02-10 17:20:56 -05:00
Jeff Avallone 5984f59063 Adding Message component 2018-02-10 17:20:04 -05:00
Jeff Avallone 7ef40cba9c Adding app frame 2018-02-10 17:01:30 -05:00
Jeff Avallone f8aaeba7a9 Going to one template for all pages 2018-02-10 16:39:38 -05:00
Jeff Avallone 8eb4b450ca Adding SVG icons 2018-02-10 16:32:52 -05:00
Jeff Avallone e52103a516 Integrating React 2018-02-10 16:22:59 -05:00
Jeff Avallone 5e8501b25e Adding humans and robots files 2018-02-10 16:12:11 -05:00
Jeff Avallone 40d08ddef8 Adding service worker cache 2018-02-10 16:08:42 -05:00
Jeff Avallone 97a61dc0f3 Adding prod and dev webpack configs 2018-02-10 16:05:25 -05:00
Jeff Avallone d78f4efd16 Base webpack config 2018-02-10 15:58:54 -05:00
Jeff Avallone c67101a209 Updating .gitignore 2018-02-10 15:58:54 -05:00
Jeff Avallone ae2b7c74dd Clearing out old site code 2018-02-10 13:33:13 -05:00
197 changed files with 9605 additions and 15194 deletions
-88
View File
@@ -1,88 +0,0 @@
{
"env": {
"browser": true,
"es6": true,
"node": true,
"jest/globals": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:jest/recommended"
],
"parser": "babel-eslint",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"react",
"jest"
],
"rules": {
"arrow-parens": [
"error",
"as-needed"
],
"arrow-spacing": [
"error",
{
"before": true,
"after": true
}
],
"comma-dangle": [
"error",
{
"objects": "never",
"arrays": "never",
"imports": "never",
"exports": "never",
"functions": "never"
}
],
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"max-len": [
"warn",
{
"code": 80
}
],
"no-var": "error",
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"space-before-function-paren": [
"error",
{
"named": "never",
"anonymous": "never",
"asyncArrow": "always"
}
],
"template-curly-spacing": [
"error",
"always"
]
},
"settings": {
"react": {
"version": "16.7"
}
}
}
-17
View File
@@ -1,17 +0,0 @@
{
"projects": {
"default": "regexper"
},
"targets": {
"regexper": {
"hosting": {
"production": [
"regexper"
],
"preview": [
"regexper-preview"
]
}
}
}
}
+6 -7
View File
@@ -18,13 +18,12 @@ node_modules/
# Yarn Integrity file
.yarn-integrity
# Gatsby build files
.cache/
public/
# Build output
build/
script/__build__/
# Test coverage
# Coverage reports
coverage/
# Firebase
.firebase/
firebase-debug.log
# Favicon cache
.wwp-cache/
+39 -24
View File
@@ -1,55 +1,68 @@
image: node:latest
stages:
- setup
- test
- build
- deploy
.shared_runner: &shared_runner
tags:
- shared
.cache_consumer: &cache_consumer
cache:
policy: pull
paths:
- node_modules
.preview_job: &preview_job
only:
- gatsby # TODO: Change to master once merged
- react # TODO: Change to master once merged
.production_job: &production_job
only:
- /^release-.*$/
.build_template: &build_template
<<: *shared_runner
<<: *cache_consumer
stage: build
dependencies: []
artifacts:
paths:
- public/
- build/
script:
- yarn build
.deploy_template: &deploy_template
<<: *shared_runner
<<: *cache_consumer
stage: deploy
script:
- yarn firebase use --token $FIREBASE_DEPLOY_KEY default
- yarn firebase deploy --only hosting:$DEPLOY_ENV -m "Pipeline $CI_PIPELINE_ID, Build $CI_BUILD_ID" --non-interactive --token $FIREBASE_DEPLOY_KEY
- yarn deploy
cache:
setup:
<<: *shared_runner
stage: setup
cache:
paths:
- node_modules/
before_script:
- node_modules
script:
- yarn install
test-lint:
stage: test
script:
- yarn test:lint
test-unit:
test:
<<: *shared_runner
<<: *cache_consumer
stage: test
coverage: '/^Statements\s*:\s*([^%]+)/'
script:
- yarn test:unit
artifacts:
paths:
- coverage/
script:
- yarn test
build-preview:
build_preview:
<<: *build_template
<<: *preview_job
variables:
@@ -57,31 +70,33 @@ build-preview:
DEPLOY_ENV: preview
GA_PROPERTY: $PREVIEW_GA_PROPERTY
build-production:
build_production:
<<: *build_template
<<: *production_job
variables:
DEPLOY_ENV: production
GA_PROPERTY: $PROD_GA_PROPERTY
deploy-preview:
deploy_preview:
<<: *deploy_template
<<: *preview_job
dependencies:
- build-preview
- build_preview
environment:
name: preview
url: https://preview.regexper.com
variables:
DEPLOY_ENV: preview
CLOUD_FRONT_ID: $PREVIEW_CLOUDFRONT_ID
DEPLOY_BUCKET: $PREVIEW_DEPLOY_BUCKET
deploy-production:
deploy_production:
<<: *deploy_template
<<: *production_job
dependencies:
- build-production
- build_production
environment:
name: production
url: https://regexper.com
variables:
DEPLOY_ENV: production
CLOUD_FRONT_ID: $PROD_CLOUDFRONT_ID
DEPLOY_BUCKET: $PROD_DEPLOY_BUCKET
+1
View File
@@ -0,0 +1 @@
registry "https://registry.yarnpkg.com"
+37
View File
@@ -20,6 +20,43 @@ To start a development server, run:
$ yarn start
### Translating
A helper tool is available to support maintaining translations. Running `yarn i18n:scrub` will update locale data files under `src/locales` by normalizing YAML syntax across all the files, maintaining a `missing.yaml` file under each locale, and indicating any existing translations that appear to be no longer used. For this, the `src/locales/en/translation.yaml` file is used as a source of truth.
To add a new locale, first create a directory for the locale under `src/locales` (no files need to be added at this point), then run `yarn i18n:scrub`. This will create a `src/locales/<locale name>/missing.yaml` file that contains required translations. Add a `src/locales/<locale name>/translation.yaml` file and at a minimum add a translation for the `/displayName` value. This should be the translated name of the locale your are adding.
If you are looking to translation missing text for a locale that is already present in the project, start by running `yarn i18n:scrub`. Check the `src/locales/<locale name>/missing.yaml` file for translations that are needed. Add any updated translations to `src/locales/<locale name>/translation.yaml`.
Before committing any updated translations, run `yarn i18n:scrub` again to maintain syntax in the YAML files and remove any translated entries from `missing.yaml`. Commit any changes to the `translation.yaml` and `missing.yaml` files.
### Available scripts
* `yarn start` - Start a development server on port 8080
* `yarn start:prod` - Run a build and start a web server on port 8080. This will not automatically rebuild.
* `yarn build` - Run a production build (used for deployments and for rebuilding when running `yarn start:prod`)
* `yarn deploy` - Deploy application to AWS S3 bucket
* `yarn test` - Run lint and unit tests
* `yarn test:lint` - Run eslint
* `yarn test:unit` - Run jest unit tests
* `yarn test:watch` - Run jest in watch mode
* `yarn test:bundle-analyzer` - Generate webpack-bundle-analyzer report
* `yarn i18n:scrub` - Scrubs i18n locale configs. Adds missing keys and normalizes YAML formatting
## Configuration
Several environment variables are used to configure the application at build-time. None of these values are required during testing.
* `NODE_ENV` - Effects build-time optimizations. Set to `"production"` for builds and unit tests in package.json. Setting to anything else will show a banner in the application's header.
* `GA_PROPERTY` - Google Analytics property ID.
* `SENTRY_KEY` - Sentry.io DSN key for error reporting.
* `CI_COMMIT_REF_SLUG`, `CI_COMMIT_SHA` - GitLab CI values used to generate build ID. Displayed in application footer and used in Sentry.io error reports.
* `CLOUD_FRONT_ID` - AWS CloudFront distribution ID to invalidating when running `yarn deploy`
* `DEPLOY_BUCKET` - AWS S3 bucket to deploy application to when running `yarn deploy`.
* `DEPLOY_ENV` - Environment the application will be deployed to. Used to report environment in Sentry.io error reports. Typically set to either "preview" or "production".
* `BANNER` - Text to display in header banner. Generally generated from `NODE_ENV`
* `BUILD_ID` - Application build ID. Generated from `CI_COMMIT_REF_SLUG` and `CI_COMMIT_SHA` if not set.
## License
See [LICENSE.txt](/LICENSE.txt) file for licensing details.
+21
View File
@@ -0,0 +1,21 @@
const path = require('path');
module.exports = {
s3Bucket: process.env.DEPLOY_BUCKET,
cloudFrontId: process.env.CLOUD_FRONT_ID,
deployFrom: path.resolve(__dirname, 'build'),
paths: [
{
match: /^service-worker.js/,
CacheControl: 'max-age=0'
},
{
match: /^(js|css|icons-\w{8})/,
CacheControl: 'public, max-age=31536000'
},
{ // Default config. MUST BE LAST
match: /./,
CacheControl: 'public, max-age=96400'
}
]
};
-12
View File
@@ -1,12 +0,0 @@
{
"hosting": [
{
"target": "production",
"public": "public"
},
{
"target": "preview",
"public": "public"
}
]
}
-33
View File
@@ -1,33 +0,0 @@
import React from 'react';
import Modal from 'react-modal';
import * as Sentry from '@sentry/browser';
import { I18nextProvider } from 'react-i18next';
import i18n from 'i18n';
import Layout from 'components/Layout';
import 'site.css';
import style from 'globals.module.css';
Modal.setAppElement('#___gatsby');
Modal.defaultProps = {
...Modal.defaultProps,
className: style.modal,
overlayClassName: style.modalOverlay
};
export const onClientEntry = () => {
Sentry.getCurrentHub().getClient().getOptions().enabled =
(navigator.doNotTrack !== '1' && window.doNotTrack !== '1');
};
// eslint-disable-next-line react/prop-types
export const wrapPageElement = ({ element }) => {
return <Layout>{ element }</Layout>;
};
// eslint-disable-next-line react/prop-types
export const wrapRootElement = ({ element }) => {
return <I18nextProvider i18n={ i18n }>{ element }</I18nextProvider>;
};
-58
View File
@@ -1,58 +0,0 @@
const pkg = require('./package.json');
const buildId = [
process.env.CI_COMMIT_REF_SLUG || 'prerelese',
(process.env.CI_COMMIT_SHA || 'gitsha').slice(0, 7)
].join('-');
const banner = process.env.BANNER || (process.env.NODE_ENV === 'production'
? false
: (process.env.NODE_ENV || 'development'));
module.exports = {
siteMetadata: {
description: pkg.description,
buildId,
banner,
defaultSyntax: 'js',
syntaxList: [
{ id: 'js', label: 'JavaScript' },
{ id: 'pcre', label: 'PCRE' }
]
},
plugins: [
'gatsby-plugin-react-helmet',
'gatsby-plugin-postcss',
{
resolve: 'gatsby-plugin-google-analytics',
options: {
trackingId: process.env.GA_PROPERTY,
anonymize: true,
respectDNT: true,
storeGac: false,
cookieExpires: 0
}
},
{
resolve: 'gatsby-plugin-sentry',
options: {
dsn: process.env.SENTRY_DSN,
environment: process.env.DEPLOY_ENV || process.env.NODE_ENV,
debug: (process.env.NODE_ENV !== 'production'),
release: buildId
}
},
{
resolve: 'gatsby-plugin-manifest',
options: {
name: 'Regexper',
short_name: 'Regexper',
start_url: '/',
background_color: '#6b6659',
theme_color: '#bada55',
display: 'standalone',
icon: 'src/icon.svg'
}
},
'gatsby-plugin-offline'
]
};
-9
View File
@@ -1,9 +0,0 @@
const path = require('path');
exports.onCreateWebpackConfig = ({ actions }) => {
actions.setWebpackConfig({
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules']
}
});
};
-15
View File
@@ -1,15 +0,0 @@
import React from 'react';
import { I18nextProvider } from 'react-i18next';
import i18n from 'i18n';
import Layout from 'components/Layout';
// eslint-disable-next-line react/prop-types
export const wrapPageElement = ({ element }) => {
return <Layout>{ element }</Layout>;
};
// eslint-disable-next-line react/prop-types
export const wrapRootElement = ({ element }) => {
return <I18nextProvider i18n={ i18n }>{ element }</I18nextProvider>;
};
-6
View File
@@ -1,6 +0,0 @@
const babelOptions = {
presets: ['babel-preset-gatsby'],
plugins: ['dynamic-import-node']
};
module.exports = require('babel-jest').createTransformer(babelOptions);
-5
View File
@@ -1,5 +0,0 @@
global.___loader = {
enqueue: jest.fn()
};
global.Element.prototype.getBBox = jest.fn();
-7
View File
@@ -1,7 +0,0 @@
const path = require('path');
module.exports = {
process(src, filename) {
return `module.exports = ${ JSON.stringify(path.basename(filename)) };`;
}
};
-7
View File
@@ -1,7 +0,0 @@
const yaml = require('js-yaml');
module.exports = {
process(src) {
return `module.exports = ${ JSON.stringify(yaml.safeLoad(src)) };`;
}
};
+143 -72
View File
@@ -1,7 +1,7 @@
{
"name": "regexper",
"version": "1.0.0",
"description": "Regular expression visualization tool",
"description": "Regular expression visualization tool using railroad diagrams",
"homepage": "http://regexper.com",
"author": {
"name": "Jeffrey Avallone",
@@ -10,21 +10,39 @@
"license": "MIT",
"private": true,
"scripts": {
"start": "gatsby develop",
"build": "gatsby build",
"start": "webpack-dev-server --config webpack.dev.js",
"start:prod": "run-s build start:http-server",
"start:http-server": "http-server -c0 ./build",
"build": "cross-env NODE_ENV=production run-s build:webpack:web build:webpack:prerender build:prerender",
"build:webpack:web": "webpack --config webpack.prod-web.js",
"build:webpack:prerender": "webpack --config webpack.prod-prerender.js",
"build:prerender": "node ./script/__build__/prerender.js",
"deploy": "node ./script/s3-upload.js deploy.config.js",
"test": "run-s test:lint 'test:unit --coverage'",
"test:unit": "cross-env NODE_ENV=production jest",
"test:lint": "eslint --ignore-path .gitignore .",
"test:unit": "jest --coverage",
"test:watch": "jest --watch"
},
"husky": {
"hooks": {
"pre-commit": "yarn test:lint"
}
"test:watch": "yarn test:unit --watch",
"test:bundle-analyzer": "cross-env NODE_ENV=production webpack --config webpack.bundle-analyzer.js",
"i18n:scrub": "node ./script/i18n-scrub.js",
"precommit": "run-s test:lint"
},
"browserslist": [
">1%",
"not ie < 11"
],
"babel": {
"presets": [
"env",
"react"
],
"plugins": [
"transform-runtime",
"transform-class-properties",
"transform-object-rest-spread",
"transform-decorators-legacy",
"syntax-dynamic-import"
]
},
"postcss": {
"plugins": {
"postcss-import": {},
@@ -37,79 +55,132 @@
},
"jest": {
"clearMocks": true,
"collectCoverageFrom": [
"src/**/*.js",
"!src/i18n.js"
"setupTestFrameworkScriptFile": "<rootDir>/src/setup/jest.js",
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"coverageReporters": [
"text-summary",
"html"
],
"globals": {
"__PATH_PREFIX__": ""
},
"moduleNameMapper": {
"\\.css$": "identity-obj-proxy"
},
"modulePaths": [
"src",
"node_modules"
],
"setupFilesAfterEnv": [
"react-testing-library/cleanup-after-each",
"<rootDir>/jest/setup.js"
],
"testPathIgnorePatterns": [
"node_modules",
".cache"
],
"transform": {
"\\.yaml$": "<rootDir>/jest/yaml.js",
"\\.js$": "<rootDir>/jest/preprocess.js",
"\\.svg$": "<rootDir>/jest/static-file-transform.js"
"moduleNameMapper": {
"\\.svg$": "__mocks__/svgMock.js",
"\\.css$": "identity-obj-proxy"
},
"transformIgnorePatterns": [
"node_modules/(?!(gatsby)/)"
"collectCoverageFrom": [
"src/**/*.js",
"!src/i18n.js",
"!src/prerender.js",
"!src/setup/service-worker.js",
"!src/setup/jest.js",
"!src/pages/**/config.js",
"!src/pages/**/browser.js"
],
"watchPathIgnorePatterns": [
"<rootDir>/coverage",
"<rootDir>/public"
"coverageReporters": [
"text-summary",
"html"
]
},
"eslintConfig": {
"env": {
"browser": true,
"es6": true,
"node": true,
"jest/globals": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module"
},
"plugins": [
"react",
"jest"
],
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
},
"dependencies": {
"@babel/core": "^7.2.2",
"@ungap/url-search-params": "^0.1.2",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^24.5.0",
"babel-plugin-dynamic-import-node": "^2.2.0",
"babel-preset-gatsby": "^0.1.6",
"eslint": "^5.11.1",
"eslint-plugin-jest": "^22.1.2",
"eslint-plugin-react": "^7.12.1",
"firebase-tools": "^6.3.0",
"gatsby": "^2.0.81",
"gatsby-plugin-google-analytics": "^2.0.8",
"gatsby-plugin-manifest": "^2.0.13",
"gatsby-plugin-offline": "^2.0.21",
"gatsby-plugin-postcss": "^2.0.2",
"gatsby-plugin-react-helmet": "^3.0.5",
"gatsby-plugin-sentry": "^1.0.0",
"husky": "^1.3.1",
"i18next": "^15.0.7",
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^2.0.1",
"@alienfast/i18next-loader": "^1.0.14",
"aws-sdk": "^2.247.1",
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.1",
"babel-jest": "^23.0.1",
"babel-loader": "^7.1.2",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-register": "^6.26.0",
"cheerio": "^1.0.0-rc.2",
"colors": "^1.1.2",
"copy-webpack-plugin": "^4.4.1",
"cross-env": "^5.1.3",
"css-loader": "^0.28.9",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"enzyme-to-json": "^3.3.1",
"eslint": "^4.17.0",
"eslint-plugin-jest": "^21.8.0",
"eslint-plugin-react": "^7.6.1",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"feather-icons": "^4.5.0",
"html-webpack-plugin": "^3.0.0",
"i18next": "^11.3.2",
"i18next-browser-languagedetector": "^2.1.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.5.0",
"js-yaml": "^3.13.0",
"immutable": "^3.8.2",
"jest": "^23.1.0",
"mime-types": "^2.1.18",
"npm-run-all": "^4.1.2",
"postcss-cssnext": "^3.1.0",
"postcss-import": "^12.0.1",
"prop-types": "^15.6.2",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-feather": "^1.1.5",
"react-helmet": "^5.2.0",
"react-i18next": "^10.5.3",
"react-modal": "^3.8.1",
"react-testing-library": "^6.0.2"
"postcss-import": "^11.1.0",
"postcss-loader": "^2.1.0",
"raven-js": "^3.22.2",
"react": "^16.3.0",
"react-dom": "^16.3.0",
"react-ga": "^2.4.1",
"react-i18next": "^7.3.6",
"react-test-renderer": "^16.3.0",
"recursive-readdir": "^2.2.2",
"style-loader": "^0.21.0",
"svg-react-loader": "^0.4.5",
"uglifyjs-webpack-plugin": "^1.1.8",
"url-search-params": "^0.10.0",
"webapp-webpack-plugin": "^2.1.0",
"webpack": "^4.1.1",
"webpack-cli": "^3.0.1",
"webpack-merge": "^4.1.1",
"webpack-node-externals": "^1.6.0",
"workbox-webpack-plugin": "^3.0.1"
},
"devDependencies": {
"http-server": "^0.11.1",
"husky": "^0.14.3",
"js-yaml": "^3.10.0",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-dev-server": "^3.1.1"
}
}
+16
View File
@@ -0,0 +1,16 @@
# humanstxt.org/
# The humans responsible & technology colophon
# TEAM
Creator: Jeff Avallone
Site: http://gitlab.com/javallone
Twitter: @javallone
# THANKS
strfriend.com for the idea, whatever happened to you?
# TECHNOLOGY COLOPHON
HTML5, CSS3, SVG, React, Feather Icons
+3
View File
@@ -0,0 +1,3 @@
# robotstxt.org/
User-agent: *
+102
View File
@@ -0,0 +1,102 @@
const util = require('util');
const path = require('path');
const fs = require('fs');
const yaml = require('js-yaml');
const colors = require('colors/safe');
const readdir = util.promisify(fs.readdir);
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
const localesDir = path.resolve(__dirname, '../src/locales');
const loadLocales = async () => {
const languages = (await readdir(localesDir)).filter(name => name !== 'index.js');
let localeData = {};
await Promise.all(languages.map(async lang => {
const langDir = path.resolve(localesDir, lang);
const namespaces = (await readdir(langDir))
.filter(file => /\.yaml$/.test(file));
localeData[lang] = {};
await Promise.all(namespaces.map(async ns => {
const nsFile = path.resolve(langDir, ns);
localeData[lang][ns.replace('.yaml', '')] = yaml.safeLoad(await readFile(nsFile));
}));
}));
return localeData;
};
const saveLocales = async locales => {
await Promise.all(Object.keys(locales).map(async langName => {
const lang = locales[langName];
await Promise.all(Object.keys(lang).map(async nsName => {
const nsFile = path.resolve(localesDir, langName, `${ nsName }.yaml`);
const yamlDump = yaml.safeDump(lang[nsName], {
sortKeys: true
});
await writeFile(nsFile, yamlDump);
}));
}));
};
loadLocales()
.then(async locales => {
const sourceLocale = locales.en.translation;
const requiredKeys = Object.keys(sourceLocale);
const languages = Object.keys(locales).filter(lang => lang !== 'en');
languages.forEach(langName => {
const lang = locales[langName];
const presentKeys = Object.keys(lang).filter(nsName => nsName !== 'missing').reduce((list, nsName) => {
return list.concat(Object.keys(lang[nsName]));
}, []);
const missingKeys = requiredKeys.filter(key => !presentKeys.includes(key));
const extraKeys = presentKeys.filter(key => !requiredKeys.includes(key));
if (!lang.translation) {
lang.translation = {};
}
if (!lang.missing) {
lang.missing = {};
}
missingKeys.forEach(key => {
if (lang.missing[key]) {
return;
}
console.log(colors.yellow.bold('MISSING:'), `${ langName } needs value for "${ colors.bold(key) }".`); //eslint-disable-line no-console
lang.missing[key] = sourceLocale[key];
});
presentKeys.forEach(key => {
if (!lang.missing[key]) {
return;
}
console.log(colors.yellow.bold('DEFINED:'), `Removing "${ colors.bold(key) }" from ${ langName}.missing. It is defined elsewhere.`); // eslint-disable-line no-console
delete lang.missing[key];
});
extraKeys.forEach(key => {
console.log(colors.yellow.bold('EXTRA:'), `${ langName } has extra key for "${ colors.bold(key) }". It should be removed.`); // eslint-disable-line no-console
});
});
return locales;
})
.then(saveLocales)
.then(() => {
console.log('Done updating locales'); // eslint-disable-line no-console
})
.catch(e => {
console.error(colors.red.bold('FAILED:'), e); // eslint-disable-line no-console
process.exit(1);
});
+35
View File
@@ -0,0 +1,35 @@
// NOTE: This script *MUST* be built with webpack since it requires React
// components. The script is built and run as part of `yarn build`
import React from 'react';
import { renderToString } from 'react-dom/server';
import fs from 'fs';
import util from 'util';
import cheerio from 'cheerio';
import colors from 'colors/safe';
import 'i18n';
const readdir = util.promisify(fs.readdir);
const readFile = util.promisify(fs.readFile);
const writeFile = util.promisify(fs.writeFile);
readdir('./src/pages')
.then(pages => (
Promise.all(pages.map(async page => {
const Component = (await import(`pages/${ page }/Component`)).default;
const pagePath = `./build/${ page }.html`;
const markup = cheerio.load(await readFile(pagePath));
markup('#root').html(renderToString(<Component/>));
await writeFile(pagePath, markup.html());
console.log(colors.green.bold('PRERENDERED:'), `${ page }.html`); // eslint-disable-line no-console
}))
))
.then(() => console.log('Done prerendering')) // eslint-disable-line no-console
.catch(e => {
console.error(colors.red.bold('FAIL:'), e); // eslint-disable-line no-console
process.exit(1);
});
+101
View File
@@ -0,0 +1,101 @@
/* eslint-disable no-console */
const path = require('path');
const fs = require('fs');
const colors = require('colors/safe');
const readdir = require('recursive-readdir');
const AWS = require('aws-sdk');
const mime = require('mime-types');
const s3 = new AWS.S3();
const cloudFront = new AWS.CloudFront();
const config = require(path.resolve(process.argv[2]));
const configFor = path => {
const { match, ...conf } = config.paths.find(conf => conf.match.test(path)); // eslint-disable-line no-unused-vars
return {
ContentType: mime.lookup(path) || 'application/octet-stream',
...conf
};
};
const bucketContents = s3.listObjectsV2({
Bucket: config.s3Bucket
}).promise()
.then(result => {
return result.Contents.map(item => item.Key);
})
.catch(err => {
console.error(colors.red.bold('Failed to fetch bucket contents:'), err);
process.exit(1);
});
const uploadDetails = readdir(config.deployFrom)
.then(paths => paths.map(p => {
const key = path.relative(config.deployFrom, p);
return {
Key: key,
Body: fs.createReadStream(p),
...configFor(key)
};
}))
.catch(err => {
console.error(colors.red.bold('Error:'), err);
process.exit(1);
});
Promise.all([bucketContents, uploadDetails]).then(([bucket, upload]) => {
const deleteKeys = bucket.filter(key => !upload.find(conf => key === conf.Key));
const uploadPromises = upload.map(params => {
console.log(`Starting upload for ${ params.Key }`);
return s3.upload({
Bucket: config.s3Bucket,
...params
}).promise()
.then(() => console.log(colors.green(`${ params.Key } successful`)))
.catch(err => {
console.error(colors.red.bold(`${ params.Key } failed`));
return Promise.reject(err);
});
});
return Promise.all(uploadPromises)
.then(() => {
if (deleteKeys.length === 0) {
console.log('No files to delete');
return Promise.resolve();
}
console.log(`Deleting ${ deleteKeys.length } stale files`);
return s3.deleteObjects({
Bucket: config.s3Bucket,
Delete: {
Objects: deleteKeys.map(key => ({ Key: key }))
}
}).promise()
.then(() => console.log(colors.green('Delete successful')))
.catch(err => {
console.error(colors.red.bold('Delete failed'));
return Promise.reject(err);
});
})
.then(() => {
return cloudFront.createInvalidation({
DistributionId: config.cloudFrontId,
InvalidationBatch: {
CallerReference: `deploy-${ process.env.CI_COMMIT_REF_SLUG }-${ process.env.CI_COMMIT_SHA }`,
Paths: {
Quantity: 1,
Items: [
'/*'
]
}
}
}).promise();
});
})
.catch(err => {
console.error(colors.red.bold('Error:'), err);
process.exit(1);
});
+18
View File
@@ -0,0 +1,18 @@
/* eslint-disable react/prop-types */
import React from 'react';
import reflowable from 'components/SVG/reflowable';
@reflowable
class SVGElement extends React.PureComponent {
reflow() {
return this.setBBox(this.props.bbox);
}
render() {
return <text>Mock content</text>;
}
}
export default SVGElement;
-19
View File
@@ -1,19 +0,0 @@
const React = require('react');
const buildMock = component => {
const componentName = component.displayName || component.name || 'Component';
const Mock = ({ children, ...props }) => (
<span
data-component={ componentName }
data-props={ JSON.stringify(props, null, ' ') }>{ children }</span>
);
Mock.propTypes = component.propTypes;
return Mock;
};
module.exports = path => {
const actual = jest.requireActual(path);
return buildMock(actual.default || actual);
};
module.exports.buildMock = buildMock;
-14
View File
@@ -1,14 +0,0 @@
const React = require('react');
const gatsby = jest.requireActual('gatsby');
module.exports = {
...gatsby,
graphql: jest.fn().mockImplementation(([query]) => query),
Link: jest.fn().mockImplementation(({ to, ...rest }) =>
React.createElement('a', {
...rest,
href: to
})
),
StaticQuery: jest.fn()
};
+18 -13
View File
@@ -1,15 +1,20 @@
const i18n = jest.requireActual('i18n');
import React from 'react';
import i18n from 'i18next';
import { I18nextProvider } from 'react-i18next';
// Load empty resource bundle to reduce logging output
i18n.default.addResourceBundle('dev', 'translation', {});
i18n.default.addResourceBundle('en', 'translation', {});
i18n.default.addResourceBundle('other', 'translation', {});
const translate = txt => `translate(${ txt })`;
module.exports = {
...i18n,
locales: [
{ code: 'en', name: 'English' },
{ code: 'other', name: 'Other' }
],
mockT: str => `TRANSLATE(${ str })`
};
i18n.init({
fallbackLng: 'en',
fallbackNS: 'missing',
debug: false,
resources: {}
});
const I18nWrapper = ({ children }) => ( // eslint-disable-line react/prop-types
<I18nextProvider i18n={ i18n }>
{ React.cloneElement(React.Children.only(children), { t: translate }) }
</I18nextProvider>
);
export { translate, i18n, I18nWrapper };
-8
View File
@@ -1,8 +0,0 @@
const reactI18next = jest.requireActual('react-i18next');
const i18n = require('i18n');
module.exports = {
...reactI18next,
Trans: require('__mocks__/component-mock').buildMock(reactI18next.Trans),
useTranslation: () => ({ i18n, t: i18n.mockT })
};
+5
View File
@@ -0,0 +1,5 @@
import React from 'react';
const SvgMock = () => <svg></svg>;
export default SvgMock;
-87
View File
@@ -1,87 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`layout running against a node with a basic bounding box 1`] = `
Object {
"box": Object {
"axisX1": 0,
"axisX2": 20,
"axisY": 5,
"height": 10,
"width": 20,
},
"props": Object {},
"type": "Example",
}
`;
exports[`layout running against a node with a complete bounding box 1`] = `
Object {
"box": Object {
"axisX1": 5,
"axisX2": 15,
"axisY": 2,
"height": 10,
"width": 20,
},
"props": Object {},
"type": "Example",
}
`;
exports[`layout running against a node with props 1`] = `
Object {
"box": Object {
"axisX1": 0,
"axisX2": 0,
"axisY": 0,
"height": 0,
"width": 0,
},
"props": Object {
"property": "example",
},
"type": "Example",
}
`;
exports[`layout running against a simple node 1`] = `
Object {
"box": Object {
"axisX1": 0,
"axisX2": 0,
"axisY": 0,
"height": 0,
"width": 0,
},
"props": Object {},
"type": "Example",
}
`;
exports[`layout running layout on children 1`] = `
Object {
"box": Object {
"axisX1": 0,
"axisX2": 0,
"axisY": 0,
"height": 0,
"width": 0,
},
"children": Array [
Object {
"box": Object {
"axisX1": 0,
"axisX2": 20,
"axisY": 5,
"height": 10,
"width": 20,
},
"props": Object {},
"type": "Other",
},
"string example",
],
"props": Object {},
"type": "Example",
}
`;
+10 -248
View File
@@ -1,255 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App removing rendered expression 1`] = `
<DocumentFragment>
<span
data-component="Form"
data-props="{
\\"syntax\\": \\"js\\",
\\"expr\\": \\"test expression\\",
\\"syntaxList\\": [
{
\\"id\\": \\"testJS\\",
\\"label\\": \\"Testing JS\\"
},
{
\\"id\\": \\"other\\",
\\"label\\": \\"Other\\"
}
]
}"
>
<span
data-component="FormActions"
data-props="{}"
/>
</span>
<span
data-component="Render"
data-props="{
\\"data\\": \\"LAYOUT(PARSED(test expression))\\"
}"
/>
</DocumentFragment>
`;
exports[`App removing rendered expression 2`] = `
<DocumentFragment>
<span
data-component="Form"
data-props="{
\\"syntax\\": \\"js\\",
\\"expr\\": \\"\\",
\\"syntaxList\\": [
{
\\"id\\": \\"testJS\\",
\\"label\\": \\"Testing JS\\"
},
{
\\"id\\": \\"other\\",
\\"label\\": \\"Other\\"
}
]
}"
/>
</DocumentFragment>
`;
exports[`App rendering 1`] = `
<DocumentFragment>
<span
data-component="Form"
data-props="{
\\"syntax\\": \\"js\\",
\\"expr\\": \\"\\",
\\"syntaxList\\": [
{
\\"id\\": \\"testJS\\",
\\"label\\": \\"Testing JS\\"
},
{
\\"id\\": \\"other\\",
\\"label\\": \\"Other\\"
<React.Fragment>
<Translate(Form)
downloadUrls={Array []}
key="expr=undefined&syntax=undefined"
onSubmit={[Function]}
syntaxes={
Object {
"js": "JavaScript",
"pcre": "PCRE",
}
]
}"
/>
</DocumentFragment>
`;
exports[`App rendering an expression 1`] = `
<DocumentFragment>
<span
data-component="Form"
data-props="{
\\"syntax\\": \\"js\\",
\\"expr\\": \\"\\",
\\"syntaxList\\": [
{
\\"id\\": \\"testJS\\",
\\"label\\": \\"Testing JS\\"
},
{
\\"id\\": \\"other\\",
\\"label\\": \\"Other\\"
}
]
}"
/>
</DocumentFragment>
`;
exports[`App rendering an expression 2`] = `
<DocumentFragment>
<span
data-component="Form"
data-props="{
\\"syntax\\": \\"js\\",
\\"expr\\": \\"test expression\\",
\\"syntaxList\\": [
{
\\"id\\": \\"testJS\\",
\\"label\\": \\"Testing JS\\"
},
{
\\"id\\": \\"other\\",
\\"label\\": \\"Other\\"
}
]
}"
/>
<span
data-component="Loader"
data-props="{}"
/>
</DocumentFragment>
`;
exports[`App rendering an expression 3`] = `
<DocumentFragment>
<span
data-component="Form"
data-props="{
\\"syntax\\": \\"js\\",
\\"expr\\": \\"test expression\\",
\\"syntaxList\\": [
{
\\"id\\": \\"testJS\\",
\\"label\\": \\"Testing JS\\"
},
{
\\"id\\": \\"other\\",
\\"label\\": \\"Other\\"
}
]
}"
>
<span
data-component="FormActions"
data-props="{}"
/>
</span>
<span
data-component="Render"
data-props="{
\\"data\\": \\"LAYOUT(PARSED(test expression))\\"
}"
/>
</DocumentFragment>
`;
exports[`App rendering with an invalid syntax 1`] = `
<DocumentFragment>
<span
data-component="Form"
data-props="{
\\"syntax\\": \\"invalid\\",
\\"expr\\": \\"\\",
\\"syntaxList\\": [
{
\\"id\\": \\"testJS\\",
\\"label\\": \\"Testing JS\\"
},
{
\\"id\\": \\"other\\",
\\"label\\": \\"Other\\"
}
]
}"
/>
</DocumentFragment>
`;
exports[`App rendering with an invalid syntax 2`] = `
<DocumentFragment>
<span
data-component="Form"
data-props="{
\\"syntax\\": \\"invalid\\",
\\"expr\\": \\"test expression\\",
\\"syntaxList\\": [
{
\\"id\\": \\"testJS\\",
\\"label\\": \\"Testing JS\\"
},
{
\\"id\\": \\"other\\",
\\"label\\": \\"Other\\"
}
]
}"
/>
<span
data-component="Loader"
data-props="{}"
/>
</DocumentFragment>
`;
exports[`App rendering with an invalid syntax 3`] = `
<DocumentFragment>
<span
data-component="Form"
data-props="{
\\"syntax\\": \\"invalid\\",
\\"expr\\": \\"test expression\\",
\\"syntaxList\\": [
{
\\"id\\": \\"testJS\\",
\\"label\\": \\"Testing JS\\"
},
{
\\"id\\": \\"other\\",
\\"label\\": \\"Other\\"
}
]
}"
/>
<span
data-component="Message"
data-props="{
\\"type\\": \\"error\\",
\\"heading\\": \\"TRANSLATE(Render Failure)\\"
}"
>
<p>
<span
data-component="Trans"
data-props="{}"
>
An error occurred while rendering the regular expression.
</span>
</p>
<a
href="#retry"
>
<span
data-component="Trans"
data-props="{}"
>
Retry
</span>
</a>
</span>
</DocumentFragment>
</React.Fragment>
`;
+186 -120
View File
@@ -1,165 +1,231 @@
import React from 'react';
import PropTypes from 'prop-types';
import { withTranslation, Trans } from 'react-i18next';
import * as Sentry from '@sentry/browser';
import URLSearchParams from '@ungap/url-search-params';
import { translate } from 'react-i18next';
import URLSearchParams from 'url-search-params';
import Raven from 'raven-js';
import LoaderIcon from 'feather-icons/dist/icons/loader.svg';
import style from './style.css';
import Form from 'components/Form';
import FormActions from 'components/FormActions';
import Loader from 'components/Loader';
import Message from 'components/Message';
import InstallPrompt from 'components/InstallPrompt';
import { demoImage } from 'devel';
const syntaxes = {
js: 'JavaScript',
pcre: 'PCRE'
};
const toUrl = params => new URLSearchParams(params).toString();
class App extends React.PureComponent {
static propTypes = {
syntax: PropTypes.string.isRequired,
expr: PropTypes.string.isRequired,
permalinkUrl: PropTypes.string,
syntaxList: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
label: PropTypes.string
})),
t: PropTypes.func.isRequired
}
state = {}
state = {
loading: false,
loadingError: null,
render: {}
}
image = React.createRef()
componentDidMount() {
if (this.props.expr) {
this.handleRender();
}
window.addEventListener('hashchange', this.handleHashChange);
window.addEventListener('beforeinstallprompt', this.handleInstallPrompt);
this.handleHashChange();
}
componentDidUpdate(prevProps) {
const { syntax, expr } = this.props;
if (syntax !== prevProps.syntax || expr !== prevProps.expr) {
this.handleRender();
}
componentWillUnmount() {
window.removeEventListener('hashchange', this.handleHashChange);
window.removeEventListener('beforeinstallprompt', this.handleInstallPrompt);
}
handleSubmit = ({ syntax, expr }) => {
if (expr) {
document.location.hash = new URLSearchParams({
syntax,
expr
}).toString();
}
}
handleRender = async () => {
const { syntax, expr } = this.props;
async setSvgUrl() {
try {
const type = 'image/svg+xml';
const blob = await this.image.current.svgUrl(type);
this.setState({
loading: false,
loadingError: null,
render: {}
svgUrl: {
url: URL.createObjectURL(blob),
label: 'Download SVG',
filename: 'image.svg',
type
}
});
}
catch (e) {
console.error(e); // eslint-disable-line no-console
}
}
async setPngUrl() {
try {
const type = 'image/png';
const blob = await this.image.current.pngUrl(type);
this.setState({
pngUrl: {
url: URL.createObjectURL(blob),
label: 'Download PNG',
filename: 'image.png',
type
}
});
}
catch (e) {
console.error(e); // eslint-disable-line no-console
}
}
async loadSVGComponent() {
if (this.state.SVG) {
return;
}
this.setState({
loading: true,
loadingFailed: false
});
try {
const SVG = await import(/* webpackChunkName: "render" */ 'components/SVG');
this.setState({
SVG: SVG.default,
loading: false
});
}
catch (e) {
Raven.captureException(e);
this.setState({
loading: false,
loadingFailed: e
});
throw e;
}
}
handleInstallPrompt = event => {
event.preventDefault();
this.setState({
installPrompt: event
});
}
handleSubmit = ({expr, syntax}) => {
if (expr) {
document.location.hash = toUrl({ syntax, expr });
}
}
handleHashChange = async () => {
const query = document.location.hash.slice(1);
const params = new URLSearchParams(query);
const { expr, syntax } = (() => {
if (params.get('syntax')) {
return {
syntax: params.get('syntax'),
expr: params.get('expr')
};
} else {
// Assuming old-style URL
return {
syntax: 'js',
expr: query
};
}
})();
if (!expr) {
return;
}
this.setState({
loading: true
});
try {
const syntaxModule = await import(
/* webpackChunkName: "render-[index]" */
`syntax/${ syntax }`
);
const exprData = syntaxModule.layout(syntaxModule.parse(expr));
await this.loadSVGComponent();
console.log(syntax, expr); // eslint-disable-line no-console
this.setState({
loading: false,
render: {
image: demoImage,
permalinkUrl: document.location.toString(),
syntax,
exprData,
Component: syntaxModule.Render
}
expr
}, async () => {
await this.image.current.doReflow();
this.setSvgUrl();
this.setPngUrl();
});
}
catch (e) {
Sentry.withScope(scope => {
scope.setExtra('syntax', syntax);
Sentry.captureException(e);
});
this.setState({
loading: false,
loadingError: e
});
// eslint-disable-next-line no-console
console.error(e);
console.error(e); // eslint-disable-line no-console
}
}
handleRetry = event => {
handleRetry = async event => {
event.preventDefault();
this.handleRender();
this.handleHashChange();
}
handleSvg = imageDetails => this.setState({ imageDetails });
handleInstallReject = () => {
this.setState({ installPrompt: null });
}
handleInstallAccept = async () => {
const { installPrompt } = this.state;
this.setState({ installPrompt: null });
installPrompt.prompt();
}
render() {
const {
syntax,
expr,
permalinkUrl,
syntaxList,
t
} = this.props;
const {
SVG,
loading,
loadingError,
imageDetails,
render: {
syntax: renderSyntax,
exprData,
Component
}
} = this.state;
const formProps = {
onSubmit: this.handleSubmit,
loadingFailed,
svgUrl,
pngUrl,
permalinkUrl,
syntax,
expr,
syntaxList
};
const actionProps = {
imageDetails,
permalinkUrl
};
const renderProps = {
onRender: this.handleSvg,
data: exprData
};
image,
installPrompt
} = this.state;
const downloadUrls = [
svgUrl,
pngUrl
].filter(Boolean);
const doRender = renderSyntax === syntax;
return <>
<Form { ...formProps }>
{ doRender && <FormActions { ...actionProps } /> }
</Form>
{ loading && <Loader /> }
{ loadingError && <Message type="error" heading={ t('Render Failure') }>
<p><Trans>
An error occurred while rendering the regular expression.
</Trans></p>
<a href="#retry" onClick={ this.handleRetry }><Trans>Retry</Trans></a>
</Message> }
{ doRender && <Component { ...renderProps } /> }
</>;
return <React.Fragment>
<Form
key={ toUrl({ expr, syntax }) }
syntaxes={ syntaxes }
downloadUrls={ downloadUrls }
permalinkUrl={ permalinkUrl }
syntax={ syntax }
expr={ expr }
onSubmit={ this.handleSubmit }/>
{
loading && <div className={ style.loader }>
<LoaderIcon />
<div className={ style.message }>Loading...</div>
</div>
}
{
loadingFailed && <Message type="error" heading="Render Failure">
An error occurred while rendering the regular expression. <a href="#retry" onClick={ this.handleRetry }>Retry</a>
</Message>
}
{
image && <div className={ style.render }>
<SVG data={ image } ref={ this.image }/>
</div>
}
{
installPrompt && <InstallPrompt onAccept={ this.handleInstallAccept } onReject={ this.handleInstallReject } />
}
</React.Fragment>;
}
}
App.propTypes = {
t: PropTypes.func
};
export default translate()(App);
export { App };
export default withTranslation()(App);
@@ -1,11 +1,24 @@
@import url('../../globals.module.css');
@import url('../../globals.css');
.render {
width: 100%;
background: var(--color-white);
box-sizing: border-box;
overflow: auto;
margin: var(--spacing-margin) 0;
& svg {
display: block;
transform: scaleZ(1); /* Move to separate render layer in Chrome */
margin: 0 auto;
}
}
.loader {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: var(--spacing-margin) 0;
padding: 2rem;
background: var(--color-white);
color: var(--color-black);
@@ -13,7 +26,6 @@
& .message {
font-weight: bold;
font-size: 2.5rem;
margin-top: 2rem;
padding: 0;
text-align: center;
}
@@ -21,8 +33,8 @@
& svg {
display: block;
transform: scaleZ(1); /* Move to separate render layer in Chrome */
width: 4rem;
height: 4rem;
width: 5rem;
height: 5rem;
stroke: var(--color-black);
animation: loader-spin 1s steps(8) infinite;
+6 -80
View File
@@ -1,90 +1,16 @@
jest.mock('components/Form', () =>
require('__mocks__/component-mock')('components/Form'));
jest.mock('components/FormActions', () =>
require('__mocks__/component-mock')('components/FormActions'));
jest.mock('components/Loader', () =>
require('__mocks__/component-mock')('components/Loader'));
jest.mock('components/Message', () =>
require('__mocks__/component-mock')('components/Message'));
jest.mock('components/SVG');
import React from 'react';
import { render } from 'react-testing-library';
import { shallow } from 'enzyme';
import { mockT } from 'i18n';
import { App } from 'components/App';
jest.mock('syntax/js', () => ({
parse: expr => `PARSED(${ expr })`,
layout: parsed => `LAYOUT(${ parsed })`,
Render: require('__mocks__/component-mock').buildMock(function Render() {})
}));
const syntaxList = [
{ id: 'testJS', label: 'Testing JS' },
{ id: 'other', label: 'Other' }
];
const commonProps = { syntaxList, t: mockT };
import { translate } from '__mocks__/i18n';
describe('App', () => {
test('rendering', () => {
const { asFragment } = render(
<App expr="" syntax="js" { ...commonProps } />
const component = shallow(
<App t={ translate }/>
);
expect(asFragment()).toMatchSnapshot();
});
test('rendering an expression', async () => {
const { asFragment, rerender } = render(
<App expr="" syntax="js" { ...commonProps } />
);
expect(asFragment()).toMatchSnapshot();
rerender(
<App expr="test expression" syntax="js" { ...commonProps } />
);
expect(asFragment()).toMatchSnapshot();
// Give a beat for module to load
await new Promise(resolve => setTimeout(resolve));
expect(asFragment()).toMatchSnapshot();
});
test('rendering with an invalid syntax', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
const { asFragment, rerender } = render(
<App expr="" syntax="invalid" { ...commonProps } />
);
expect(asFragment()).toMatchSnapshot();
rerender(
<App expr="test expression" syntax="invalid" { ...commonProps } />
);
expect(asFragment()).toMatchSnapshot();
// Give a beat for module to load
await new Promise(resolve => setTimeout(resolve));
expect(asFragment()).toMatchSnapshot();
});
test('removing rendered expression', async () => {
const { asFragment, rerender } = render(
<App expr="test expression" syntax="js" { ...commonProps } />
);
// Give a beat for module to load
await new Promise(resolve => setTimeout(resolve));
expect(asFragment()).toMatchSnapshot();
rerender(
<App expr="" syntax="js" { ...commonProps } />
);
expect(asFragment()).toMatchSnapshot();
expect(component).toMatchSnapshot();
});
});
@@ -1,30 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Footer rendering 1`] = `
<DocumentFragment>
<footer
class="footer"
>
<ul
class="list"
>
<footer>
<ul>
<li>
<span
data-component="Trans"
data-props="{}"
>
<Trans>
Created by
<a
href="mailto:jeff.avallone@gmail.com"
>
Jeff Avallone
</a>
</span>
</Trans>
</li>
<li>
<span
data-component="Trans"
data-props="{}"
<Trans
i18nKey="Generated images licensed"
>
Generated images licensed:
<a
@@ -33,18 +24,17 @@ exports[`Footer rendering 1`] = `
target="_blank"
>
<img
alt="TRANSLATE(Creative Commons CC-BY-3.0 License)"
src="cc-by.svg"
alt="Creative Commons CC-BY-3.0 License"
src="https://licensebuttons.net/l/by/3.0/80x15.png"
/>
</a>
</span>
</Trans>
</li>
</ul>
<div
class="buildId"
className="buildId"
>
abc-123
example build id
</div>
</footer>
</DocumentFragment>
</footer>
`;
-121
View File
@@ -1,121 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="80"
height="15"
id="svg2279"
sodipodi:version="0.32"
inkscape:version="0.45+devel"
version="1.0"
sodipodi:docname="by.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape">
<defs
id="defs2281">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath3442">
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.92243534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect3444"
width="20.614058"
height="12.483703"
x="171.99832"
y="239.1203" />
</clipPath>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#999999"
borderopacity="1"
gridtolerance="10000"
guidetolerance="10"
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="10.5125"
inkscape:cx="40"
inkscape:cy="7.5"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="80px"
height="15px"
showborder="true"
inkscape:showpageshadow="false"
inkscape:window-width="935"
inkscape:window-height="624"
inkscape:window-x="50"
inkscape:window-y="160" />
<metadata
id="metadata2284">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="BY"
transform="matrix(0.9875019,0,0,0.9333518,-323.90064,-271.87688)">
<g
transform="translate(158,54)"
id="g3693">
<rect
y="237.86218"
x="170.5"
height="15"
width="80"
id="rect3695"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.04161763;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.92243534;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect3697"
width="77"
height="12"
x="172"
y="239.36218" />
<path
sodipodi:nodetypes="cccscc"
id="path3699"
d="M 171.99996,239.37505 L 171.99996,251.37505 L 192.33474,251.37505 C 193.64339,249.62474 194.52652,247.59057 194.52652,245.37505 C 194.52652,243.17431 193.65859,241.1179 192.36599,239.37505 L 171.99996,239.37505 z"
style="fill:#abb1aa;fill-opacity:1;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.46913578" />
<g
clip-path="url(#clipPath3442)"
transform="matrix(0.9612533,0,0,0.9612533,6.8341566,9.5069994)"
id="g3701">
<path
style="opacity:1;fill:#ffffff"
d="M 190.06417,245.36206 C 190.06667,249.25405 186.91326,252.41072 183.02153,252.41323 C 179.12979,252.41572 175.97262,249.26256 175.97036,245.3706 C 175.97036,245.36783 175.97036,245.36507 175.97036,245.36206 C 175.9681,241.47007 179.12126,238.3134 183.013,238.31113 C 186.90524,238.30864 190.06191,241.46181 190.06417,245.3538 C 190.06417,245.35628 190.06417,245.35929 190.06417,245.36206 z"
rx="22.939548"
type="arc"
cy="264.3577"
ry="22.939548"
cx="296.35416"
id="path3703" />
<path
style="opacity:1"
id="path3705"
d="M 188.74576,239.62226 C 190.30843,241.18492 191.08988,243.09869 191.08988,245.36206 C 191.08988,247.62592 190.32197,249.51913 188.78615,251.04165 C 187.15627,252.64521 185.22995,253.44672 183.00722,253.44672 C 180.81132,253.44672 178.91837,252.65172 177.32887,251.06174 C 175.73912,249.47198 174.94436,247.57226 174.94436,245.36206 C 174.94436,243.15235 175.73912,241.23908 177.32887,239.62226 C 178.87799,238.0591 180.77094,237.27764 183.00722,237.27764 C 185.2706,237.27764 187.18312,238.05909 188.74576,239.62226 z M 178.38093,240.67355 C 177.05978,242.008 176.39945,243.57116 176.39945,245.36429 C 176.39945,247.15694 177.05326,248.70682 178.36062,250.01393 C 179.66822,251.32153 181.22487,251.97509 183.03105,251.97509 C 184.83724,251.97509 186.40716,251.31502 187.74161,249.99412 C 189.0086,248.76725 189.64234,247.22467 189.64234,245.36429 C 189.64234,243.51799 188.99831,241.95084 187.71101,240.66354 C 186.42396,239.37649 184.86406,238.7327 183.03105,238.7327 C 181.19804,238.73271 179.64767,239.37975 178.38093,240.67355 z M 181.85761,244.57559 C 181.65573,244.13545 181.35354,243.91525 180.95051,243.91525 C 180.23802,243.91525 179.8819,244.39501 179.8819,245.35404 C 179.8819,246.31328 180.23802,246.79255 180.95051,246.79255 C 181.421,246.79255 181.75705,246.55908 181.95869,246.09111 L 182.94629,246.61701 C 182.47555,247.45339 181.76934,247.87168 180.82763,247.87168 C 180.10136,247.87168 179.51953,247.64899 179.08265,247.20409 C 178.64502,246.7587 178.42684,246.14477 178.42684,245.36206 C 178.42684,244.59313 178.65204,243.98271 179.10271,243.53056 C 179.55338,243.07838 180.11463,242.8524 180.7875,242.8524 C 181.78288,242.8524 182.49561,243.24465 182.92647,244.02835 L 181.85761,244.57559 z M 186.50398,244.57559 C 186.30184,244.13545 186.00567,243.91525 185.61517,243.91525 C 184.88839,243.91525 184.52474,244.39501 184.52474,245.35404 C 184.52474,246.31328 184.88839,246.79255 185.61517,246.79255 C 186.08642,246.79255 186.41644,246.55908 186.6048,246.09111 L 187.61447,246.61701 C 187.14448,247.45339 186.43926,247.87168 185.49931,247.87168 C 184.77403,247.87168 184.19346,247.64899 183.75683,247.20409 C 183.32096,246.7587 183.10254,246.14477 183.10254,245.36206 C 183.10254,244.59313 183.32422,243.98271 183.76737,243.53056 C 184.21026,243.07838 184.77404,242.8524 185.4592,242.8524 C 186.45282,242.8524 187.16455,243.24465 187.5939,244.02835 L 186.50398,244.57559 z" />
</g>
</g>
<path
id="text3707"
d="M 357.4197,298.68502 C 357.66518,298.68503 357.85131,298.63145 357.9781,298.52427 C 358.10488,298.41711 358.16827,298.25904 358.16828,298.05007 C 358.16827,297.84377 358.10488,297.68704 357.9781,297.57987 C 357.85131,297.47003 357.66518,297.41511 357.4197,297.4151 L 356.55784,297.4151 L 356.55784,298.68502 L 357.4197,298.68502 M 357.4723,301.30928 C 357.78522,301.30928 358.0199,301.24363 358.17637,301.11235 C 358.33552,300.98108 358.4151,300.78282 358.4151,300.51758 C 358.4151,300.2577 358.33686,300.06346 358.18041,299.93486 C 358.02396,299.80358 357.78792,299.73795 357.4723,299.73794 L 356.55784,299.73794 L 356.55784,301.30928 L 357.4723,301.30928 M 358.92089,299.15121 C 359.25538,299.24766 359.51434,299.42582 359.69779,299.6857 C 359.88121,299.94558 359.97293,300.26439 359.97294,300.64216 C 359.97293,301.22086 359.776,301.6522 359.38217,301.9362 C 358.98833,302.22019 358.38947,302.36218 357.5856,302.36218 L 355.00001,302.36218 L 355.00001,296.36218 L 357.33878,296.36218 C 358.17771,296.36219 358.78466,296.48811 359.15962,296.73995 C 359.53727,296.9918 359.7261,297.39501 359.7261,297.94959 C 359.7261,298.24163 359.65732,298.49079 359.51975,298.69708 C 359.38217,298.9007 359.18255,299.05208 358.92089,299.15121 M 359.83746,296.36218 L 361.54096,296.36218 L 362.91671,298.50016 L 364.29245,296.36218 L 366,296.36218 L 363.69764,299.83439 L 363.69764,302.36218 L 362.13982,302.36218 L 362.13982,299.83439 L 359.83746,296.36218"
style="font-size:8.25858784px;font-style:normal;font-weight:bold;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:'Bitstream Vera Sans'" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.1 KiB

+18 -29
View File
@@ -1,38 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation, Trans } from 'react-i18next';
import { translate, Trans } from 'react-i18next';
import ccLogo from './cc-by.svg';
import style from './style.css';
import style from './style.module.css';
export const Footer = ({ buildId }) => {
const { t } = useTranslation();
return <footer className={ style.footer }>
<ul className={ style.list }>
const Footer = () => (
<footer>
<ul>
<li>
<Trans>Created by <a
href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a></Trans>
<Trans>Created by <a href="mailto:jeff.avallone@gmail.com">Jeff Avallone</a></Trans>
</li>
<li>
<Trans>Generated images licensed: <a
href="http://creativecommons.org/licenses/by/3.0/"
rel="license external noopener noreferrer"
target="_blank">
<img src={ ccLogo }
alt={ t('Creative Commons CC-BY-3.0 License') } />
</a></Trans>
<Trans i18nKey="Generated images licensed">
Generated images licensed: <a rel="license external noopener noreferrer" target="_blank" href="http://creativecommons.org/licenses/by/3.0/">
<img
alt="Creative Commons CC-BY-3.0 License"
src="https://licensebuttons.net/l/by/3.0/80x15.png" />
</a>
</Trans>
</li>
</ul>
<div className={ style.buildId }>
{ buildId }
</div>
</footer>;
};
<div className={ style.buildId }>{ process.env.BUILD_ID }</div>
</footer>
);
Footer.propTypes = {
buildId: PropTypes.string.isRequired
};
export default Footer;
export default translate()(Footer);
export { Footer };
@@ -1,6 +1,6 @@
@import url('../../globals.module.css');
@import url('../../globals.css');
.footer {
footer {
display: flex;
align-items: flex-start;
margin: var(--spacing-margin) 0;
@@ -9,18 +9,19 @@
display: block;
}
& ul {
@apply --inline-list;
@apply --with-separator-left;
flex: 1;
}
& img {
vertical-align: text-top;
width: 80px;
height: 15px;
}
}
.list {
composes: inline-list with-separator-left;
flex: 1;
}
.buildId {
& .buildId {
color: color(var(--color-brown) blend(var(--color-tan) 25%));
}
}
+9 -5
View File
@@ -1,13 +1,17 @@
import React from 'react';
import { render } from 'react-testing-library';
import { shallow } from 'enzyme';
import Footer from 'components/Footer';
import { Footer } from 'components/Footer';
describe('Footer', () => {
beforeEach(() => {
process.env.BUILD_ID = 'example build id';
});
test('rendering', () => {
const { asFragment } = render(
<Footer buildId="abc-123" />
const component = shallow(
<Footer/>
);
expect(asFragment()).toMatchSnapshot();
expect(component).toMatchSnapshot();
});
});
+166 -26
View File
@@ -1,54 +1,194 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Form rendering 1`] = `
<DocumentFragment>
<div
class="form"
data-requires-js="true"
>
<div
className="form"
>
<form
data-testid="form"
onSubmit={[Function]}
>
<textarea
data-testid="expr-input"
autoFocus={true}
name="expr"
placeholder="TRANSLATE(Enter regular expression to display)"
onChange={[Function]}
onKeyPress={[Function]}
placeholder="translate(Enter regular expression to display)"
/>
<button
type="submit"
>
<span
data-component="Trans"
data-props="{}"
>
<Trans>
Display
</span>
</Trans>
</button>
<div
class="select"
className="select"
>
<select
data-testid="syntax-select"
name="syntax"
onChange={[Function]}
value="js"
>
<option
value="testJS"
key="js"
value="js"
>
TRANSLATE(Testing JS)
Javascript
</option>
<option
value="other"
key="pcre"
value="pcre"
>
TRANSLATE(Other)
PCRE
</option>
</select>
<span
data-component="ChevronsDown"
data-props="{}"
<SvgMock />
</div>
<ul
className="actions"
/>
</div>
Actions
</form>
</div>
</DocumentFragment>
</div>
`;
exports[`Form rendering with download URLs 1`] = `
<div
className="form"
>
<form
onSubmit={[Function]}
>
<textarea
autoFocus={true}
name="expr"
onChange={[Function]}
onKeyPress={[Function]}
placeholder="translate(Enter regular expression to display)"
/>
<button
type="submit"
>
<Trans>
Display
</Trans>
</button>
<div
className="select"
>
<select
name="syntax"
onChange={[Function]}
value="js"
>
<option
key="js"
value="js"
>
Javascript
</option>
<option
key="pcre"
value="pcre"
>
PCRE
</option>
</select>
<SvgMock />
</div>
<ul
className="actions"
>
<li
key="0"
>
<a
download="image.svg"
href="#svg"
type="image/svg+xml"
>
<SvgMock />
<Trans>
Download SVG
</Trans>
</a>
</li>
<li
key="1"
>
<a
download="image.png"
href="#png"
type="image/png"
>
<SvgMock />
<Trans>
Download PNG
</Trans>
</a>
</li>
</ul>
</form>
</div>
`;
exports[`Form rendering with permalink URL 1`] = `
<div
className="form"
>
<form
onSubmit={[Function]}
>
<textarea
autoFocus={true}
name="expr"
onChange={[Function]}
onKeyPress={[Function]}
placeholder="translate(Enter regular expression to display)"
/>
<button
type="submit"
>
<Trans>
Display
</Trans>
</button>
<div
className="select"
>
<select
name="syntax"
onChange={[Function]}
value="js"
>
<option
key="js"
value="js"
>
Javascript
</option>
<option
key="pcre"
value="pcre"
>
PCRE
</option>
</select>
<SvgMock />
</div>
<ul
className="actions"
>
<li>
<a
href="#permalink"
>
<SvgMock />
<Trans>
Permalink
</Trans>
</a>
</li>
</ul>
</form>
</div>
`;
+78 -46
View File
@@ -1,73 +1,105 @@
import React, { useState, useCallback } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation, Trans } from 'react-i18next';
import { translate, Trans } from 'react-i18next';
import ExpandIcon from 'react-feather/dist/icons/chevrons-down';
import DownloadIcon from 'feather-icons/dist/icons/download.svg';
import LinkIcon from 'feather-icons/dist/icons/link.svg';
import ExpandIcon from 'feather-icons/dist/icons/chevrons-down.svg';
import style from './style.module.css';
import style from './style.css';
const Form = ({ syntaxList, children, onSubmit, ...props }) => {
const { t } = useTranslation();
const [ expr, exprUpdate ] = useState(props.expr);
const [ syntax, syntaxUpdate ] = useState(props.syntax);
const handleExprChange = useCallback(event => {
exprUpdate(event.target.value);
}, [exprUpdate]);
const handleSyntaxChange = useCallback(event => {
syntaxUpdate(event.target.value);
}, [syntaxUpdate]);
const handleSubmit = useCallback(event => {
event.preventDefault();
onSubmit({ expr, syntax });
}, [expr, syntax, onSubmit]);
const handleKeyPress = useCallback(event => {
if (event.charCode === 13 && event.shiftKey) {
handleSubmit(event);
class Form extends React.PureComponent {
state = {
expr: this.props.expr,
syntax: this.props.syntax || Object.keys(this.props.syntaxes)[0]
}
}, [handleSubmit]);
return <div className={ style.form } data-requires-js>
<form data-testid="form" onSubmit={ handleSubmit }>
handleSubmit = event => {
event.preventDefault();
this.props.onSubmit.call(this, {
expr: this.state.expr,
syntax: this.state.syntax
});
}
handleKeyPress = event => {
if (event.charCode === 13 && event.shiftKey) {
this.handleSubmit(event);
}
}
handleChange = event => this.setState({ [event.target.name]: event.target.value })
permalinkAction() {
const { permalinkUrl } = this.props;
if (!permalinkUrl) {
return;
}
return <li>
<a href={ permalinkUrl }><LinkIcon/><Trans>Permalink</Trans></a>
</li>;
}
downloadActions() {
const { downloadUrls } = this.props;
if (!downloadUrls) {
return;
}
return downloadUrls.map(({ url, filename, type, label }, i) => <li key={ i }>
<a href={ url } download={ filename } type={ type }>
<DownloadIcon/><Trans>{ label }</Trans>
</a>
</li>);
}
render() {
const { syntaxes, t } = this.props;
const { expr, syntax } = this.state;
return <div className={ style.form }>
<form onSubmit={ this.handleSubmit }>
<textarea
data-testid="expr-input"
name="expr"
value={ expr }
onKeyPress={ handleKeyPress }
onChange={ handleExprChange }
onKeyPress={ this.handleKeyPress }
onChange={ this.handleChange }
autoFocus
placeholder={ t('Enter regular expression to display') }></textarea>
<button type="submit"><Trans>Display</Trans></button>
<div className={ style.select }>
<select
data-testid="syntax-select"
name="syntax"
value={ syntax }
onChange={ handleSyntaxChange } >
{ syntaxList.map(({ id, label }) => (
<option value={ id } key={ id }>{ t(label) }</option>
onChange={ this.handleChange }>
{ Object.keys(syntaxes).map(id => (
<option value={ id } key={ id }>{ syntaxes[id] }</option>
)) }
</select>
<ExpandIcon />
<ExpandIcon/>
</div>
{ children }
<ul className={ style.actions }>
{ this.downloadActions() }
{ this.permalinkAction() }
</ul>
</form>
</div>;
};
}
}
Form.propTypes = {
expr: PropTypes.string,
syntax: PropTypes.string,
syntaxList: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
label: PropTypes.string
})),
onSubmit: PropTypes.func.isRequired,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
])
syntaxes: PropTypes.object,
onSubmit: PropTypes.func,
permalinkUrl: PropTypes.string,
downloadUrls: PropTypes.array,
t: PropTypes.func
};
export default Form;
export default translate()(Form);
export { Form };
@@ -1,4 +1,4 @@
@import url('../../globals.module.css');
@import url('../../globals.css');
:root {
--control-gradient: var(--color-green) var(--gradient-green);
@@ -23,7 +23,7 @@
width: 100% !important; /* "!important" to prevent user changing width */
height: calc(3 * var(--entry-line-height));
box-sizing: border-box;
font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
}
& textarea::placeholder {
@@ -44,6 +44,24 @@
}
}
.select {
composes: fancy-select;
.actions {
@apply --list-reset;
margin-top: var(--spacing-margin);
@media (min-width: 700px) {
@apply --inline-list;
@apply --with-separator-left;
float: right;
}
& svg {
width: 1em;
height: 1em;
margin-right: 0.5rem;
vertical-align: middle;
}
}
.select {
@apply --fancy-select;
}
+57 -44
View File
@@ -1,80 +1,93 @@
jest.mock('react-feather/dist/icons/chevrons-down', () =>
require('__mocks__/component-mock')(
'react-feather/dist/icons/chevrons-down'));
import React from 'react';
import { render, fireEvent } from 'react-testing-library';
import { shallow } from 'enzyme';
import Form from 'components/Form';
import { Form } from 'components/Form';
import { translate } from '__mocks__/i18n';
const syntaxList = [
{ id: 'testJS', label: 'Testing JS' },
{ id: 'other', label: 'Other' }
];
const commonProps = { syntaxList };
const syntaxes = {
js: 'Javascript',
pcre: 'PCRE'
};
describe('Form', () => {
test('rendering', () => {
const { asFragment } = render(
<Form onSubmit={ jest.fn() } { ...commonProps }>
Actions
</Form>
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes }/>
);
expect(asFragment()).toMatchSnapshot();
expect(component).toMatchSnapshot();
});
test('rendering with download URLs', () => {
const downloadUrls = [
{ url: '#svg', filename: 'image.svg', type: 'image/svg+xml', label: 'Download SVG' },
{ url: '#png', filename: 'image.png', type: 'image/png', label: 'Download PNG' }
];
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes } downloadUrls={ downloadUrls }/>
);
expect(component).toMatchSnapshot();
});
test('rendering with permalink URL', () => {
const permalinkUrl = '#permalink';
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes } permalinkUrl={ permalinkUrl }/>
);
expect(component).toMatchSnapshot();
});
describe('submitting expression', () => {
test('submitting form', () => {
const onSubmit = jest.fn();
const { getByTestId } = render(
<Form onSubmit={ onSubmit } { ...commonProps } />
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ onSubmit }/>
);
fireEvent.change(getByTestId('expr-input'), {
target: { value: 'Test expression' }
});
fireEvent.change(getByTestId('syntax-select'), {
target: { value: 'other' }
});
const exprInput = component.find('[name="expr"]');
const syntaxInput = component.find('[name="syntax"]');
exprInput.simulate('change', { target: { name: 'expr', value: 'Test expression' } });
syntaxInput.simulate('change', { target: { name: 'syntax', value: 'test' } });
const event = new Event('submit');
jest.spyOn(event, 'preventDefault');
const eventObj = { preventDefault: jest.fn() };
component.find('form').simulate('submit', eventObj);
fireEvent(getByTestId('form'), event);
expect(event.preventDefault).toHaveBeenCalled();
expect(eventObj.preventDefault).toHaveBeenCalled();
expect(onSubmit).toHaveBeenCalledWith({
expr: 'Test expression',
syntax: 'other'
syntax: 'test'
});
});
test('submitting form with Shift+Enter', () => {
const onSubmit = jest.fn();
const { getByTestId } = render(
<Form onSubmit={ onSubmit } { ...commonProps } />
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ Function.prototype }/>
);
fireEvent.keyPress(getByTestId('expr-input'), {
const form = component.instance();
const eventObj = {
preventDefault: Function.prototype,
charCode: 13,
shiftKey: true
});
};
jest.spyOn(form, 'handleSubmit');
component.find('textarea').simulate('keypress', eventObj);
expect(onSubmit).toHaveBeenCalled();
expect(form.handleSubmit).toHaveBeenCalled();
});
test('not submitting with just Enter', () => {
const onSubmit = jest.fn();
const { getByTestId } = render(
<Form onSubmit={ onSubmit } { ...commonProps } />
const component = shallow(
<Form t={ translate } syntaxes={ syntaxes } onSubmit={ Function.protoytpe }/>
);
fireEvent.keyPress(getByTestId('expr-input'), {
const form = component.instance();
const eventObj = {
preventDefault: Function.prototype,
charCode: 13,
shiftKey: false
});
};
jest.spyOn(form, 'handleSubmit');
component.find('textarea').simulate('keypress', eventObj);
expect(onSubmit).not.toHaveBeenCalled();
expect(form.handleSubmit).not.toHaveBeenCalled();
});
});
});
@@ -1,34 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormActions rendering 1`] = `
<DocumentFragment>
<ul
class="actions"
/>
</DocumentFragment>
`;
exports[`FormActions rendering with a permalink 1`] = `
<DocumentFragment>
<ul
class="actions"
>
<li>
<a
href="http://example.com"
>
<span
data-component="Link"
data-props="{}"
/>
<span
data-component="Trans"
data-props="{}"
>
Permalink
</span>
</a>
</li>
</ul>
</DocumentFragment>
`;
-60
View File
@@ -1,60 +0,0 @@
import React, { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTranslation, Trans } from 'react-i18next';
import DownloadIcon from 'react-feather/dist/icons/download';
import LinkIcon from 'react-feather/dist/icons/link';
import style from './style.module.css';
import { createPngLink, createSvgLink } from './links';
const downloadLink = (link, t) => {
const { url, filename, type, label } = link;
return <li>
<a href={ url } download={ filename } type={ type }>
<DownloadIcon />{ t(label) }
</a>
</li>;
};
const FormActions = ({
permalinkUrl,
imageDetails
}) => {
const { t } = useTranslation();
const [svgLink, setSvgLink] = useState(null);
const [pngLink, setPngLink] = useState(null);
const generateDownloadLinks = useCallback(async () => {
const { svg, width, height } = imageDetails;
setSvgLink(await createSvgLink({ svg }));
setPngLink(await createPngLink({ svg, width, height }));
}, [setSvgLink, setPngLink, imageDetails]);
useEffect(() => {
if (imageDetails && imageDetails.svg) {
generateDownloadLinks();
}
}, [imageDetails]);
return <ul className={ style.actions }>
{ pngLink && downloadLink(pngLink, t) }
{ svgLink && downloadLink(svgLink, t) }
{ permalinkUrl && <li>
<a href={ permalinkUrl }><LinkIcon /><Trans>Permalink</Trans></a>
</li> }
</ul>;
};
FormActions.propTypes = {
permalinkUrl: PropTypes.string,
imageDetails: PropTypes.shape({
svg: PropTypes.string,
width: PropTypes.number,
height: PropTypes.number
})
};
export default FormActions;
-49
View File
@@ -1,49 +0,0 @@
const createSvgLink = async ({ svg }) => {
try {
const type = 'image/svg+xml';
const blob = new Blob([svg], { type });
return {
url: URL.createObjectURL(blob),
label: 'Download SVG',
filename: 'image.svg',
type
};
}
catch (e) {
console.error(e); // eslint-disable-line no-console
}
};
const createPngLink = async ({ svg, width, height }) => {
try {
const type = 'image/png';
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const loader = new Image();
loader.width = canvas.width = width * 2;
loader.height = canvas.height = height * 2;
await new Promise(resolve => {
loader.onload = resolve;
loader.src = 'data:image/svg+xml,' + encodeURIComponent(svg);
});
context.drawImage(loader, 0, 0, loader.width, loader.height);
const blob = await new Promise(resolve => canvas.toBlob(resolve, type));
return {
url: URL.createObjectURL(blob),
label: 'Download PNG',
filename: 'image.png',
type
};
}
catch (e) {
console.error(e); // eslint-disable-line no-console
}
};
export { createSvgLink, createPngLink };
@@ -1,19 +0,0 @@
@import url('../../globals.module.css');
.actions {
composes: inline-list with-separator-left;
float: right;
@media (max-width: 700px) {
& li {
display: block;
}
}
& svg {
width: 1em;
height: 1em;
margin-right: 0.5rem;
vertical-align: text-bottom;
}
}
-42
View File
@@ -1,42 +0,0 @@
jest.mock('./links');
jest.mock('react-feather/dist/icons/download', () =>
require('__mocks__/component-mock')(
'react-feather/dist/icons/download'));
jest.mock('react-feather/dist/icons/link', () =>
require('__mocks__/component-mock')(
'react-feather/dist/icons/link'));
import React from 'react';
import { render } from 'react-testing-library';
import FormActions from 'components/FormActions';
import { createPngLink, createSvgLink } from './links';
createPngLink.mockResolvedValue({
url: 'http://example.com/image.png',
filename: 'image.png',
type: 'image/png',
label: 'Example PNG Link'
});
createSvgLink.mockResolvedValue({
url: 'http://example.com/image.svg',
filename: 'image.svg',
type: 'image/svg+xml',
label: 'Example SVG Link'
});
describe('FormActions', () => {
test('rendering', () => {
const { asFragment } = render(
<FormActions/>
);
expect(asFragment()).toMatchSnapshot();
});
test('rendering with a permalink', () => {
const { asFragment } = render(
<FormActions permalinkUrl="http://example.com" />
);
expect(asFragment()).toMatchSnapshot();
});
});
+12 -504
View File
@@ -1,402 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header opening the Privacy Policy modal 1`] = `
<DocumentFragment>
<span
data-component="Modal"
data-props="{
\\"isOpen\\": true
}"
>
<span
data-component="PrivacyPolicy"
data-props="{}"
/>
</span>
<header
class="header"
>
<h1>
<a
href="/"
>
Regexper
</a>
</h1>
<ul
class="list"
>
<li>
<a
href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank"
>
<span
data-component="Gitlab"
data-props="{}"
/>
<span
data-component="Trans"
data-props="{}"
>
Source on GitLab
</span>
</a>
</li>
<li>
<a
data-testid="privacy-link"
href="/privacy"
>
<span
data-component="Trans"
data-props="{}"
>
Privacy Policy
</span>
</a>
</li>
<li>
<span
data-component="InstallPrompt"
data-props="{}"
/>
</li>
<li
data-requires-js="true"
>
<span
data-component="LocaleSwitcher"
data-props="{}"
/>
</li>
</ul>
</header>
</DocumentFragment>
`;
exports[`Header opening the Privacy Policy modal while holding alt key 1`] = `
<DocumentFragment>
<span
data-component="Modal"
data-props="{
\\"isOpen\\": false
}"
>
<span
data-component="PrivacyPolicy"
data-props="{}"
/>
</span>
<header
class="header"
>
<h1>
<a
href="/"
>
Regexper
</a>
</h1>
<ul
class="list"
>
<li>
<a
href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank"
>
<span
data-component="Gitlab"
data-props="{}"
/>
<span
data-component="Trans"
data-props="{}"
>
Source on GitLab
</span>
</a>
</li>
<li>
<a
data-testid="privacy-link"
href="/privacy"
>
<span
data-component="Trans"
data-props="{}"
>
Privacy Policy
</span>
</a>
</li>
<li>
<span
data-component="InstallPrompt"
data-props="{}"
/>
</li>
<li
data-requires-js="true"
>
<span
data-component="LocaleSwitcher"
data-props="{}"
/>
</li>
</ul>
</header>
</DocumentFragment>
`;
exports[`Header opening the Privacy Policy modal while holding ctrl key 1`] = `
<DocumentFragment>
<span
data-component="Modal"
data-props="{
\\"isOpen\\": false
}"
>
<span
data-component="PrivacyPolicy"
data-props="{}"
/>
</span>
<header
class="header"
>
<h1>
<a
href="/"
>
Regexper
</a>
</h1>
<ul
class="list"
>
<li>
<a
href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank"
>
<span
data-component="Gitlab"
data-props="{}"
/>
<span
data-component="Trans"
data-props="{}"
>
Source on GitLab
</span>
</a>
</li>
<li>
<a
data-testid="privacy-link"
href="/privacy"
>
<span
data-component="Trans"
data-props="{}"
>
Privacy Policy
</span>
</a>
</li>
<li>
<span
data-component="InstallPrompt"
data-props="{}"
/>
</li>
<li
data-requires-js="true"
>
<span
data-component="LocaleSwitcher"
data-props="{}"
/>
</li>
</ul>
</header>
</DocumentFragment>
`;
exports[`Header opening the Privacy Policy modal while holding meta key 1`] = `
<DocumentFragment>
<span
data-component="Modal"
data-props="{
\\"isOpen\\": false
}"
>
<span
data-component="PrivacyPolicy"
data-props="{}"
/>
</span>
<header
class="header"
>
<h1>
<a
href="/"
>
Regexper
</a>
</h1>
<ul
class="list"
>
<li>
<a
href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank"
>
<span
data-component="Gitlab"
data-props="{}"
/>
<span
data-component="Trans"
data-props="{}"
>
Source on GitLab
</span>
</a>
</li>
<li>
<a
data-testid="privacy-link"
href="/privacy"
>
<span
data-component="Trans"
data-props="{}"
>
Privacy Policy
</span>
</a>
</li>
<li>
<span
data-component="InstallPrompt"
data-props="{}"
/>
</li>
<li
data-requires-js="true"
>
<span
data-component="LocaleSwitcher"
data-props="{}"
/>
</li>
</ul>
</header>
</DocumentFragment>
`;
exports[`Header opening the Privacy Policy modal while holding shift key 1`] = `
<DocumentFragment>
<span
data-component="Modal"
data-props="{
\\"isOpen\\": false
}"
>
<span
data-component="PrivacyPolicy"
data-props="{}"
/>
</span>
<header
class="header"
>
<h1>
<a
href="/"
>
Regexper
</a>
</h1>
<ul
class="list"
>
<li>
<a
href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank"
>
<span
data-component="Gitlab"
data-props="{}"
/>
<span
data-component="Trans"
data-props="{}"
>
Source on GitLab
</span>
</a>
</li>
<li>
<a
data-testid="privacy-link"
href="/privacy"
>
<span
data-component="Trans"
data-props="{}"
>
Privacy Policy
</span>
</a>
</li>
<li>
<span
data-component="InstallPrompt"
data-props="{}"
/>
</li>
<li
data-requires-js="true"
>
<span
data-component="LocaleSwitcher"
data-props="{}"
/>
</li>
</ul>
</header>
</DocumentFragment>
`;
exports[`Header rendering 1`] = `
<DocumentFragment>
<span
data-component="Modal"
data-props="{
\\"isOpen\\": false
}"
>
<span
data-component="PrivacyPolicy"
data-props="{}"
/>
</span>
<header
class="header"
<header
className="header"
data-banner="testing"
>
>
<h1>
<a
href="/"
@@ -404,131 +12,31 @@ exports[`Header rendering 1`] = `
Regexper
</a>
</h1>
<ul
class="list"
>
<ul>
<li>
<a
href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank"
>
<span
data-component="Gitlab"
data-props="{}"
/>
<span
data-component="Trans"
data-props="{}"
>
<SvgMock />
<Trans>
Source on GitLab
</span>
</Trans>
</a>
</li>
<li>
<a
data-testid="privacy-link"
href="/privacy"
>
<span
data-component="Trans"
data-props="{}"
href="/privacy.html"
>
<Trans>
Privacy Policy
</span>
</Trans>
</a>
</li>
<li>
<span
data-component="InstallPrompt"
data-props="{}"
/>
</li>
<li
data-requires-js="true"
>
<span
data-component="LocaleSwitcher"
data-props="{}"
/>
<Translate(LocaleSwitcher) />
</li>
</ul>
</header>
</DocumentFragment>
`;
exports[`Header rendering with no banner 1`] = `
<DocumentFragment>
<span
data-component="Modal"
data-props="{
\\"isOpen\\": false
}"
>
<span
data-component="PrivacyPolicy"
data-props="{}"
/>
</span>
<header
class="header"
>
<h1>
<a
href="/"
>
Regexper
</a>
</h1>
<ul
class="list"
>
<li>
<a
href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank"
>
<span
data-component="Gitlab"
data-props="{}"
/>
<span
data-component="Trans"
data-props="{}"
>
Source on GitLab
</span>
</a>
</li>
<li>
<a
data-testid="privacy-link"
href="/privacy"
>
<span
data-component="Trans"
data-props="{}"
>
Privacy Policy
</span>
</a>
</li>
<li>
<span
data-component="InstallPrompt"
data-props="{}"
/>
</li>
<li
data-requires-js="true"
>
<span
data-component="LocaleSwitcher"
data-props="{}"
/>
</li>
</ul>
</header>
</DocumentFragment>
</header>
`;
+16 -65
View File
@@ -1,77 +1,28 @@
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import Modal from 'react-modal';
import { Link } from 'gatsby';
import { Trans } from 'react-i18next';
import React from 'react';
import { translate, Trans } from 'react-i18next';
import GitlabIcon from 'react-feather/dist/icons/gitlab';
import style from './style.css';
import GitlabIcon from 'feather-icons/dist/icons/gitlab.svg';
import LocaleSwitcher from 'components/LocaleSwitcher';
import InstallPrompt from 'components/InstallPrompt';
import PrivacyPolicy from 'components/PrivacyPolicy';
import style from './style.module.css';
const Header = ({ banner }) => {
const [ showModal, updateShowModal] = useState(false);
const handleClose = useCallback(() => {
updateShowModal(false);
}, [updateShowModal]);
const handleOpen = useCallback(event => {
if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
return;
}
event.preventDefault();
updateShowModal(true);
}, [updateShowModal]);
return <>
<Modal
isOpen={ showModal }
onRequestClose={ handleClose }>
<PrivacyPolicy onClose={ handleClose } />
</Modal>
<header
className={ style.header }
data-banner={ banner || null }>
const Header = () => (
<header className={ style.header } data-banner={ process.env.BANNER }>
<h1>
<Link to="/">Regexper</Link>
<a href="/">Regexper</a>
</h1>
<ul className={ style.list }>
<ul>
<li><a href="https://gitlab.com/javallone/regexper-static" rel="external noopener noreferrer" target="_blank">
<GitlabIcon/><Trans>Source on GitLab</Trans>
</a></li>
<li>
<a href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank">
<GitlabIcon />
<Trans>Source on GitLab</Trans>
</a>
</li>
<li>
<Link to="/privacy"
data-testid="privacy-link"
onClick={ handleOpen }
>
<Trans>Privacy Policy</Trans>
</Link>
</li>
<li>
<InstallPrompt />
</li>
<li data-requires-js>
<LocaleSwitcher />
<a href="/privacy.html"><Trans>Privacy Policy</Trans></a>
</li>
<li><LocaleSwitcher /></li>
</ul>
</header>
</>;
};
);
Header.propTypes = {
banner: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.string
]).isRequired
};
export default Header;
export default translate()(Header);
export { Header };
@@ -1,4 +1,4 @@
@import url('../../globals.module.css');
@import url('../../globals.css');
.header {
display: flex;
@@ -9,7 +9,6 @@
margin: 0 calc(-1 * var(--content-margin)) var(--spacing-margin) calc(-1 * var(--content-margin));
position: relative;
color: var(--color-black);
min-width: 320px;
&:after {
content: attr(data-banner);
@@ -42,12 +41,13 @@
text-decoration: none;
display: inline-block;
}
}
.list {
composes: inline-list with-separator-right;
& ul {
@apply --inline-list;
@apply --with-separator-right;
text-align: right;
margin: 1rem 0;
}
& li {
line-height: 2.4rem;
+9 -54
View File
@@ -1,62 +1,17 @@
jest.mock('react-modal', () =>
require('__mocks__/component-mock')('react-modal'));
jest.mock('react-feather/dist/icons/gitlab', () =>
require('__mocks__/component-mock')('react-feather/dist/icons/gitlab'));
jest.mock('components/LocaleSwitcher', () =>
require('__mocks__/component-mock')('components/LocaleSwitcher'));
jest.mock('components/InstallPrompt', () =>
require('__mocks__/component-mock')('components/InstallPrompt'));
jest.mock('components/PrivacyPolicy', () =>
require('__mocks__/component-mock')('components/PrivacyPolicy'));
import React from 'react';
import { render, fireEvent } from 'react-testing-library';
import { shallow } from 'enzyme';
import Header from 'components/Header';
import { Header } from 'components/Header';
describe('Header', () => {
beforeEach(() => {
process.env.BANNER = 'testing';
});
test('rendering', () => {
const { asFragment } = render(
<Header banner="testing" />
const component = shallow(
<Header/>
);
expect(asFragment()).toMatchSnapshot();
});
test('rendering with no banner', () => {
const { asFragment } = render(
<Header banner={ false } />
);
expect(asFragment()).toMatchSnapshot();
});
test('opening the Privacy Policy modal', () => {
const { asFragment, getByTestId } = render(
<Header banner={ false } />
);
const event = new MouseEvent('click', { bubbles: true });
jest.spyOn(event, 'preventDefault');
fireEvent(getByTestId('privacy-link'), event);
expect(event.preventDefault).toHaveBeenCalled();
expect(asFragment()).toMatchSnapshot();
});
['shift', 'ctrl', 'alt', 'meta'].forEach(key => {
test(`opening the Privacy Policy modal while holding ${ key } key`, () => {
const { asFragment, getByTestId } = render(
<Header banner={ false } />
);
const event = new MouseEvent('click', {
bubbles: true,
[key + 'Key']: true
});
jest.spyOn(event, 'preventDefault');
fireEvent(getByTestId('privacy-link'), event);
expect(event.preventDefault).not.toHaveBeenCalled();
expect(asFragment()).toMatchSnapshot();
});
expect(component).toMatchSnapshot();
});
});
@@ -1,21 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InstallPrompt rendering 1`] = `<DocumentFragment />`;
exports[`InstallPrompt rendering after an install prompt has been requested 1`] = `<DocumentFragment />`;
exports[`InstallPrompt rendering after an install prompt has been requested 2`] = `
<DocumentFragment>
<a
data-testid="install"
href="#install"
exports[`InstallPrompt rendering 1`] = `
<div
className="install"
>
<p
className="cta"
>
<span
data-component="Trans"
data-props="{}"
<Trans>
Add Regexper to your home screen?
</Trans>
</p>
<div
className="actions"
>
Add to Home Screen
</span>
</a>
</DocumentFragment>
<button
className="primary"
>
<Trans>
Add It
</Trans>
</button>
<button>
<Trans>
No Thanks
</Trans>
</button>
</div>
</div>
`;
+18 -36
View File
@@ -1,41 +1,23 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Trans } from 'react-i18next';
import React from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
const InstallPrompt = () => {
const [ installPrompt, updateInstallPrompt ] = useState(null);
import style from './style.css';
const handleInstall = useCallback(async event => {
event.preventDefault();
const InstallPrompt = ({ onAccept, onReject }) => (
<div className={ style.install }>
<p className={ style.cta }><Trans>Add Regexper to your home screen?</Trans></p>
<div className={ style.actions }>
<button className={ style.primary } onClick={ onAccept }><Trans>Add It</Trans></button>
<button onClick={ onReject }><Trans>No Thanks</Trans></button>
</div>
</div>
);
try {
installPrompt.prompt();
await installPrompt.userChoice;
}
catch {
// User cancelled install
}
updateInstallPrompt(null);
}, [installPrompt, updateInstallPrompt]);
useEffect(() => {
window.addEventListener('beforeinstallprompt', updateInstallPrompt);
return () => {
window.removeEventListener('beforeinstallprompt', updateInstallPrompt);
};
});
if (!installPrompt) {
return null;
}
return <a href="#install"
data-testid="install"
onClick={ handleInstall }
>
<Trans>Add to Home Screen</Trans>
</a>;
InstallPrompt.propTypes = {
onAccept: PropTypes.func.isRequired,
onReject: PropTypes.func.isRequired
};
export default InstallPrompt;
export default translate()(InstallPrompt);
export { InstallPrompt };
+38
View File
@@ -0,0 +1,38 @@
@import url('../../globals.css');
.install {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--color-tan);
color: var(--color-black);
}
.cta {
margin: 0;
padding: var(--spacing-margin);
text-align: center;
}
.actions {
display: flex;
flex-wrap: nowrap;
justify-content: space-evenly;
padding: var(--spacing-margin);
& button {
font-size: inherit;
line-height: 2.8rem;
border: 0 none;
background: var(--color-green) var(--gradient-green);
color: var(--color-black);
cursor: pointer;
padding: 0;
width: 40vw;
&.primary {
font-weight: bold;
}
}
}
+6 -34
View File
@@ -1,42 +1,14 @@
import React from 'react';
import { render, fireEvent } from 'react-testing-library';
import { shallow } from 'enzyme';
import InstallPrompt from 'components/InstallPrompt';
import { InstallPrompt } from 'components/InstallPrompt';
import { translate } from '__mocks__/i18n';
describe('InstallPrompt', () => {
test('rendering', () => {
const { asFragment } = render(
<InstallPrompt />
const component = shallow(
<InstallPrompt t={ translate }/>
);
expect(asFragment()).toMatchSnapshot();
});
test('rendering after an install prompt has been requested', () => {
const { asFragment } = render(
<InstallPrompt />
);
expect(asFragment()).toMatchSnapshot();
const event = new Event('beforeinstallprompt', {
prompt: jest.fn()
});
fireEvent(window, event);
expect(asFragment()).toMatchSnapshot();
});
test('removing event listener', () => {
jest.spyOn(window, 'addEventListener');
jest.spyOn(window, 'removeEventListener');
const { unmount } = render(
<InstallPrompt />
);
unmount();
expect(window.removeEventListener).toHaveBeenCalledWith(
'beforeinstallprompt',
expect.any(Function));
expect(component).toMatchSnapshot();
});
});
@@ -1,30 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Layout rendering 1`] = `
<DocumentFragment>
<span
data-component="SentryBoundary"
data-props="{}"
>
<noscript />
<span
data-component="Header"
data-props="{
\\"banner\\": \\"Test Banner\\"
}"
/>
<span
data-component="SentryBoundary"
data-props="{}"
>
Example content
</span>
<span
data-component="Footer"
data-props="{
\\"buildId\\": \\"test-buildid\\"
}"
/>
</span>
</DocumentFragment>
`;
-54
View File
@@ -1,54 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { graphql, StaticQuery } from 'gatsby';
import SentryBoundary from 'components/SentryBoundary';
import Header from 'components/Header';
import Footer from 'components/Footer';
const query = graphql`
query LayoutQuery {
site {
siteMetadata {
banner
buildId
}
}
}
`;
const noscriptStyle = `
[data-requires-js] {
display: none !important;
}
`;
export const Layout = ({ banner, buildId, children }) => <SentryBoundary>
<noscript>
<style type="text/css">{ noscriptStyle }</style>
</noscript>
<Header banner={ banner } />
<SentryBoundary>
{ children }
</SentryBoundary>
<Footer buildId={ buildId } />
</SentryBoundary>;
Layout.propTypes = {
banner: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.string
]).isRequired,
buildId: PropTypes.string.isRequired,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
// eslint-disable-next-line react/display-name
export default props => (
<StaticQuery query={ query } render={ ({ site: { siteMetadata } }) => (
<Layout { ...props } { ...siteMetadata } />
) } />
);
-22
View File
@@ -1,22 +0,0 @@
jest.mock('components/SentryBoundary', () =>
require('__mocks__/component-mock')('components/SentryBoundary'));
jest.mock('components/Header', () =>
require('__mocks__/component-mock')('components/Header'));
jest.mock('components/Footer', () =>
require('__mocks__/component-mock')('components/Footer'));
import React from 'react';
import { render } from 'react-testing-library';
import { Layout } from 'components/Layout';
describe('Layout', () => {
test('rendering', () => {
const { asFragment } = render(
<Layout banner="Test Banner" buildId="test-buildid">
Example content
</Layout>
);
expect(asFragment()).toMatchSnapshot();
});
});
@@ -1,75 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Loader rendering 1`] = `
<DocumentFragment>
<div
class="loader"
>
<svg
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12"
x2="12"
y1="2"
y2="6"
/>
<line
x1="12"
x2="12"
y1="18"
y2="22"
/>
<line
x1="4.93"
x2="7.76"
y1="4.93"
y2="7.76"
/>
<line
x1="16.24"
x2="19.07"
y1="16.24"
y2="19.07"
/>
<line
x1="2"
x2="6"
y1="12"
y2="12"
/>
<line
x1="18"
x2="22"
y1="12"
y2="12"
/>
<line
x1="4.93"
x2="7.76"
y1="19.07"
y2="16.24"
/>
<line
x1="16.24"
x2="19.07"
y1="7.76"
y2="4.93"
/>
</svg>
<div
class="message"
>
TRANSLATE(Loading...)
</div>
</div>
</DocumentFragment>
`;
-17
View File
@@ -1,17 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import LoaderIcon from 'react-feather/dist/icons/loader';
import style from './style.module.css';
const Loader = () => {
const { t } = useTranslation();
return <div className={ style.loader }>
<LoaderIcon />
<div className={ style.message }>{ t('Loading...') }</div>
</div>;
};
export default Loader;
-15
View File
@@ -1,15 +0,0 @@
import React from 'react';
import { render } from 'react-testing-library';
import Loader from 'components/Loader';
describe('Loader', () => {
test('rendering', () => {
// Using full rendering here since styles for this depend on the structure
// of the SVG.
const { asFragment } = render(
<Loader/>
);
expect(asFragment()).toMatchSnapshot();
});
});
@@ -1,36 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LocaleSwitcher rendering 1`] = `
<DocumentFragment>
<label>
<span
data-component="Trans"
data-props="{}"
>
<label>
<Trans>
Language
</span>
</Trans>
<div
class="switcher"
className="switcher"
>
<select
data-testid="language-select"
>
<option
onChange={[Function]}
value="en"
>
English
<option
key="en"
value="en"
>
/displayName
</option>
<option
value="other"
key="fr"
value="fr"
>
Other
/displayName
</option>
</select>
<span
data-component="ChevronsDown"
data-props="{}"
/>
<SvgMock />
</div>
</label>
</DocumentFragment>
</label>
`;
+46 -30
View File
@@ -1,47 +1,63 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Trans } from 'react-i18next';
import React from 'react';
import { translate, Trans } from 'react-i18next';
import i18n from 'i18next';
import ExpandIcon from 'react-feather/dist/icons/chevrons-down';
import style from './style.css';
import ExpandIcon from 'feather-icons/dist/icons/chevrons-down.svg';
import i18n, { locales } from 'i18n';
import locales from 'locales';
import localeToAvailable from './locale-to-available';
import style from './style.module.css';
const localeToAvailable = (locale, available, defaultLocale) => {
if (available.includes(locale)) {
return locale;
}
const LocaleSwitcher = () => {
const [ current, updateCurrent ] = useState(localeToAvailable(
i18n.language || '',
locales.map(l => l.code),
'en'));
const parts = locale.split('-');
useEffect(() => {
i18n.on('languageChanged', updateCurrent);
if (parts.length > 0 && available.includes(parts[0])) {
return parts[0];
}
return () => {
i18n.off('languageChanged', updateCurrent);
};
});
return defaultLocale;
};
const handleSelectChange = useCallback(({ target }) => {
class LocaleSwitcher extends React.PureComponent {
state = {
current: localeToAvailable(i18n.language || '', Object.keys(locales), 'en')
}
componentDidMount() {
i18n.on('languageChanged', this.handleLanguageChange);
}
componentWillUnmount() {
i18n.off('languageChanged', this.handleLanguageChange);
}
handleSelectChange = ({ target }) => {
i18n.changeLanguage(target.value);
});
}
handleLanguageChange = lang => {
this.setState({ current: lang });
}
render() {
const { current } = this.state;
return <label>
<Trans>Language</Trans>
<div className={ style.switcher }>
<select data-testid="language-select"
value={ current }
onChange={ handleSelectChange }
>
{ locales.map(locale => (
<option value={ locale.code } key={ locale.code }>
{ locale.name }
</option>
<select value={ current } onChange={ this.handleSelectChange }>
{ Object.keys(locales).map(locale => (
<option value={ locale } key={ locale }>{ i18n.getFixedT(locale)('/displayName') }</option>
)) }
</select>
<ExpandIcon />
<ExpandIcon/>
</div>
</label>;
};
}
}
export default LocaleSwitcher;
export default translate()(LocaleSwitcher);
export { LocaleSwitcher };
@@ -1,15 +0,0 @@
const localeToAvailable = (locale, available, defaultLocale) => {
if (available.includes(locale)) {
return locale;
}
const parts = locale.split('-');
if (parts.length > 0 && available.includes(parts[0])) {
return parts[0];
}
return defaultLocale;
};
export default localeToAvailable;
@@ -1,18 +0,0 @@
import localeToAvailable from './locale-to-available';
describe('localeToAvailable', () => {
test('when requested language and region are available', () => {
expect(localeToAvailable('en-US', ['en', 'en-US', 'other'], 'other'))
.toEqual('en-US');
});
test('when only requested language is available', () => {
expect(localeToAvailable('en-US', ['en', 'en-GB', 'other'], 'other'))
.toEqual('en');
});
test('when language is unavailable', () => {
expect(localeToAvailable('en-US', ['tlh', 'other'], 'other'))
.toEqual('other');
});
});
@@ -1,4 +1,4 @@
@import url('../../globals.module.css');
@import url('../../globals.css');
:root {
--control-gradient: var(--color-tan) var(--gradient-tan);
@@ -7,5 +7,5 @@
}
.switcher {
composes: fancy-select;
@apply --fancy-select;
}
+17 -49
View File
@@ -1,63 +1,31 @@
jest.mock('react-feather/dist/icons/chevrons-down', () =>
require('__mocks__/component-mock')(
'react-feather/dist/icons/chevrons-down'));
jest.mock('components/SVG');
jest.mock('locales', () => ({
en: {},
fr: {}
}));
import React from 'react';
import { render, fireEvent, act } from 'react-testing-library';
import { shallow } from 'enzyme';
import i18n from 'i18n';
import LocaleSwitcher from 'components/LocaleSwitcher';
// Ensure initial locale is always "en" during tests
jest.mock('./locale-to-available', () => jest.fn(() => 'en'));
import { LocaleSwitcher } from 'components/LocaleSwitcher';
import { translate } from '__mocks__/i18n';
describe('LocaleSwitcher', () => {
test('rendering', () => {
const { asFragment } = render(
<LocaleSwitcher />
const component = shallow(
<LocaleSwitcher t={ translate }/>
);
expect(asFragment()).toMatchSnapshot();
expect(component).toMatchSnapshot();
});
test('changing language', () => {
jest.spyOn(i18n, 'changeLanguage');
const { getByTestId } = render(
<LocaleSwitcher />
const component = shallow(
<LocaleSwitcher t={ translate }/>
);
const event = new Event('change', { bubbles: true });
const select = getByTestId('language-select');
select.value = 'other';
const selectInput = component.find('select');
selectInput.value = 'fr';
selectInput.simulate('change', { target: { value: 'fr' } });
fireEvent(select, event);
expect(i18n.changeLanguage).toHaveBeenCalledWith('other');
});
test('interface update from language change', () => {
const { getByTestId } = render(
<LocaleSwitcher />
);
expect(getByTestId('language-select').value).toEqual('en');
act(() => {
i18n.emit('languageChanged', 'other');
});
expect(getByTestId('language-select').value).toEqual('other');
});
test('disconnecting event handler on unmount', () => {
const { unmount } = render(
<LocaleSwitcher />
);
jest.spyOn(i18n, 'off');
unmount();
expect(i18n.off).toHaveBeenCalledWith(
'languageChanged',
expect.any(Function));
expect(component.state('current')).toEqual('fr');
});
});
@@ -1,63 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Message rendering 1`] = `
<DocumentFragment>
<div
className="message"
>
<div
class="message"
>
<div
class="header"
className="header"
>
<h2>
Testing
</h2>
</div>
<div
class="content"
className="content"
>
<p>
Message content
</p>
</div>
</div>
</DocumentFragment>
`;
exports[`Message rendering with a close button 1`] = `
<DocumentFragment>
<div
class="message"
>
<div
class="header"
>
<h2>
Testing
</h2>
<button>
<span
data-component="XSquare"
data-props="{}"
/>
Close
</button>
</div>
<div
class="content"
>
<p>
Message content
</p>
</div>
</div>
</DocumentFragment>
</div>
`;
exports[`Message rendering with icon 1`] = `
<DocumentFragment>
<div
<div
class="message"
>
>
<div
class="header"
>
@@ -73,22 +40,17 @@ exports[`Message rendering with icon 1`] = `
Message content
</p>
</div>
</div>
</DocumentFragment>
</div>
`;
exports[`Message rendering with type 1`] = `
<DocumentFragment>
<div
<div
class="message error"
>
>
<div
class="header"
>
<span
data-component="AlertOctagon"
data-props="{}"
/>
<svg />
<h2>
Testing
</h2>
@@ -100,6 +62,5 @@ exports[`Message rendering with type 1`] = `
Message content
</p>
</div>
</div>
</DocumentFragment>
</div>
`;
+10 -17
View File
@@ -1,12 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './style.module.css';
import style from './style.css';
import InfoIcon from 'react-feather/dist/icons/info';
import ErrorIcon from 'react-feather/dist/icons/alert-octagon';
import WarningIcon from 'react-feather/dist/icons/alert-triangle';
import CloseIcon from 'react-feather/dist/icons/x-square';
import InfoIcon from 'feather-icons/dist/icons/info.svg';
import ErrorIcon from 'feather-icons/dist/icons/alert-octagon.svg';
import WarningIcon from 'feather-icons/dist/icons/alert-triangle.svg';
const iconTypes = {
info: InfoIcon,
@@ -15,26 +14,21 @@ const iconTypes = {
};
const renderIcon = (type, icon) => {
const Icon = icon || iconTypes[type];
icon = icon || iconTypes[type];
if (!Icon) {
if (!icon) {
return;
}
return <Icon />;
const Icon = icon;
return <Icon/>;
};
const Message = ({ type, icon, heading, onClose, children }) => (
<div className={ [
style.message,
type && style[type]
].filter(Boolean).join(' ') }>
const Message = ({ type, icon, heading, children }) => (
<div className={ [ style.message, type && style[type] ].filter(Boolean).join(' ') }>
<div className={ style.header }>
{ renderIcon(type, icon) }
<h2>{ heading }</h2>
{ onClose && <button onClick={ onClose }>
<CloseIcon /> Close
</button> }
</div>
<div className={ style.content }>
{ children }
@@ -53,7 +47,6 @@ Message.propTypes = {
PropTypes.func
]),
heading: PropTypes.string.isRequired,
onClose: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
+72
View File
@@ -0,0 +1,72 @@
@import url('../../globals.css');
.message {
background: var(--color-tan);
color: var(--color-black);
margin: var(--spacing-margin) 0;
box-shadow: 0 0 1rem color(var(--color-black) alpha(0.7));
& .header {
& h2 {
display: inline-block;
padding: var(--spacing-margin);
line-height: 2.8rem;
margin: 0;
}
& svg {
padding: var(--spacing-margin);
height: 2.8rem;
width: 2.8rem;
vertical-align: bottom;
}
}
& .content {
padding: var(--spacing-margin);
}
& p {
margin-top: 0;
margin-bottom: var(--spacing-margin) 0;
}
& p:last-child {
margin-bottom: 0;
}
&.error {
& .header {
background: var(--color-red);
color: var(--color-white);
& svg {
background: var(--color-white);
color: var(--color-red);
}
}
}
&.warning {
& .header {
background: var(--color-orange);
}
& svg {
background: var(--color-black);
color: var(--color-orange);
}
}
&.info {
& .header {
background: var(--color-blue);
color: var(--color-white);
}
& svg {
background: var(--color-white);
color: var(--color-blue);
}
}
}
-92
View File
@@ -1,92 +0,0 @@
@import url('../../globals.module.css');
.message {
background: var(--color-tan);
color: var(--color-black);
margin: var(--spacing-margin) 0;
box-shadow: 0 0 1rem color(var(--color-black) alpha(0.7));
max-height: 100%;
display: flex;
flex-direction: column;
}
.header {
& h2 {
display: inline-block;
padding: var(--spacing-margin);
line-height: 2.8rem;
margin: 0;
}
& svg {
padding: var(--spacing-margin);
height: 2.8rem;
width: 2.8rem;
vertical-align: bottom;
}
& button {
padding: 0;
margin: 0;
border: 0 none;
background: transparent;
color: var(--color-tan);
cursor: pointer;
float: right;
font-size: 0;
&:hover,
&:active {
color: var(--color-white);
}
}
}
.content {
padding: var(--spacing-margin);
overflow: auto;
& p {
margin-top: 0;
margin-bottom: var(--spacing-margin);
}
& p:last-child {
margin-bottom: 0;
}
}
.error {
& .header {
background: var(--color-red);
color: var(--color-white);
& svg {
background: var(--color-white);
color: var(--color-red);
}
}
}
.warning {
& .header {
background: var(--color-orange);
& > svg {
background: var(--color-black);
color: var(--color-orange);
}
}
}
.info {
& .header {
background: var(--color-blue);
color: var(--color-white);
& > svg {
background: var(--color-white);
color: var(--color-blue);
}
}
}
+8 -30
View File
@@ -1,56 +1,34 @@
jest.mock('react-feather/dist/icons/info', () =>
require('__mocks__/component-mock')('react-feather/dist/icons/info'));
jest.mock('react-feather/dist/icons/alert-octagon', () =>
require('__mocks__/component-mock')(
'react-feather/dist/icons/alert-octagon'
));
jest.mock('react-feather/dist/icons/alert-triangle', () =>
require('__mocks__/component-mock')(
'react-feather/dist/icons/alert-triangle'
));
jest.mock('react-feather/dist/icons/x-square', () =>
require('__mocks__/component-mock')('react-feather/dist/icons/x-square'));
import React from 'react';
import { render } from 'react-testing-library';
import { shallow, render } from 'enzyme';
import Message from 'components/Message';
describe('Message', () => {
test('rendering', () => {
const { asFragment } = render(
<Message heading="Testing">
const component = shallow(
<Message heading="Testing" className="testing">
<p>Message content</p>
</Message>
);
expect(asFragment()).toMatchSnapshot();
expect(component).toMatchSnapshot();
});
test('rendering with icon', () => {
const Icon = () => 'Sample icon SVG';
const { asFragment } = render(
const component = render(
<Message heading="Testing" icon={ Icon }>
<p>Message content</p>
</Message>
);
expect(asFragment()).toMatchSnapshot();
expect(component).toMatchSnapshot();
});
test('rendering with type', () => {
const { asFragment } = render(
const component = render(
<Message heading="Testing" type="error">
<p>Message content</p>
</Message>
);
expect(asFragment()).toMatchSnapshot();
});
test('rendering with a close button', () => {
const { asFragment } = render(
<Message heading="Testing" onClose={ jest.fn() }>
<p>Message content</p>
</Message>
);
expect(asFragment()).toMatchSnapshot();
expect(component).toMatchSnapshot();
});
});
@@ -1,36 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Metadata rendering 1`] = `
<DocumentFragment>
<span
data-component="HelmetWrapper"
data-props="{
\\"title\\": \\"Regexper\\",
\\"htmlAttributes\\": {},
\\"meta\\": [
{
\\"name\\": \\"description\\"
}
]
}"
/>
</DocumentFragment>
`;
exports[`Metadata rendering with a title and description 1`] = `
<DocumentFragment>
<span
data-component="HelmetWrapper"
data-props="{
\\"title\\": \\"Regexper - Testing\\",
\\"htmlAttributes\\": {},
\\"meta\\": [
{
\\"name\\": \\"description\\",
\\"content\\": \\"Test description\\"
}
]
}"
/>
</DocumentFragment>
`;
-29
View File
@@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet';
const Metadata = ({ title, description }) => {
const { i18n } = useTranslation();
const helmetProps = {
title: title ? `Regexper - ${ title }` : 'Regexper',
htmlAttributes: {
lang: i18n.language
},
meta: [
{
name: 'description',
content: description
}
]
};
return <Helmet { ...helmetProps }></Helmet>;
};
Metadata.propTypes = {
title: PropTypes.string,
description: PropTypes.string
};
export default Metadata;
-30
View File
@@ -1,30 +0,0 @@
jest.mock('react-helmet', () => {
const helmet = jest.requireActual('react-helmet');
return {
...helmet,
Helmet: require('__mocks__/component-mock').buildMock(helmet.Helmet)
};
});
import React from 'react';
import { render } from 'react-testing-library';
import Metadata from 'components/Metadata';
describe('Metadata', () => {
test('rendering', () => {
const { asFragment } = render(
<Metadata/>
);
expect(asFragment()).toMatchSnapshot();
});
test('rendering with a title and description', () => {
const { asFragment } = render(
<Metadata
title="Testing"
description="Test description" />
);
expect(asFragment()).toMatchSnapshot();
});
});
-51
View File
@@ -1,51 +0,0 @@
import React from 'react';
import { useTranslation, Trans } from 'react-i18next';
import Message from 'components/Message';
export const PrivacyPolicy = props => {
const { t } = useTranslation();
return <Message type="info" heading={ t('Privacy Policy') } { ...props }>
<Trans i18nKey="Privacy policy copy">
<p>
Regexper and the tools used to create it are all open source. If you are
concerned that the JavaScript being delivered is in any way malicious,
please inspect the source in the <a
href="https://gitlab.com/javallone/regexper-static"
rel="external noopener noreferrer"
target="_blank">GitLab repository</a>.
</p>
<p>
There are two data collection tools integrated in the app. These tools
are not used to collect personal information:
</p>
<ul>
<li>
<b>Google Analytics</b> is used to track browser usage data and
application performance. It is configured to anonymize the client IP
address.
</li>
<li>
<b>Sentry.io</b> is a tool used to capture and report client-side
JavaScript errors. It is configured to not store the client IP
address.
</li>
</ul>
<p>
Regexper honors the browser <b>&ldquo;Do Not Track&rdquo;</b> setting
and will not enable these data collection tools if that setting is
enabled. Also, most popular ad blockers will prevent these tools from
sending any tracking data. Disabling or blocking these data collection
tools will <b>not</b> impact the performance of this app. The
information collected by these tools is used to monitor application
performance, determine browser support, and collect error reports.
</p>
<p>
Regexper is not supported by ad revenue or sales of any kind.
</p>
</Trans>
</Message>;
};
export default PrivacyPolicy;
-16
View File
@@ -1,16 +0,0 @@
jest.mock('components/Message', () =>
require('__mocks__/component-mock')('components/Message'));
import React from 'react';
import { render } from 'react-testing-library';
import PrivacyPolicy from 'components/PrivacyPolicy';
describe('PrivacyPolicy', () => {
test('rendering', () => {
const { asFragment } = render(
<PrivacyPolicy onClose={ jest.fn() } />
);
expect(asFragment()).toMatchSnapshot();
});
});
@@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RavenBoundary rendering (with error) 1`] = `<Child />`;
exports[`RavenBoundary rendering (with error) 2`] = `
<Translate(RavenError)
details={
Object {
"extra": Object {
"details": "test details",
},
}
}
error={
Object {
"error": "test error",
}
}
/>
`;
exports[`RavenBoundary rendering 1`] = `<Child />`;
+39
View File
@@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import RavenError from 'components/RavenError';
class RavenBoundary extends React.Component {
state = {
error: null
}
componentDidCatch(error, errorInfo) {
this.setState({ error, errorInfo });
}
render() {
const { error, errorInfo } = this.state;
const { children } = this.props;
if (error) {
const errorProps = {
details: { extra: errorInfo },
error
};
return <RavenError { ...errorProps }/>;
} else {
return children;
}
}
}
RavenBoundary.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
};
export default RavenBoundary;
+32
View File
@@ -0,0 +1,32 @@
import React from 'react';
import { shallow } from 'enzyme';
import RavenBoundary from 'components/RavenBoundary';
const testError = { error: 'test error' };
const testDetails = { details: 'test details' };
describe('RavenBoundary', () => {
test('rendering', () => {
const Child = () => <b>Child</b>;
const component = shallow(
<RavenBoundary>
<Child/>
</RavenBoundary>
);
expect(component).toMatchSnapshot();
});
test('rendering (with error)', () => {
const Child = () => <b>Child</b>;
const component = shallow(
<RavenBoundary>
<Child/>
</RavenBoundary>
);
expect(component).toMatchSnapshot();
component.instance().componentDidCatch(testError, testDetails);
component.update();
expect(component).toMatchSnapshot();
});
});
@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RavenError rendering 1`] = `
<Message
heading="translate(An error has occurred)"
type="error"
>
<p>
<Trans
i18nKey="This error has been logged"
>
This error has been logged. You may also
<a
href="#error-report"
onClick={[Function]}
>
fill out a report
</a>
.
</Trans>
</p>
</Message>
`;
+40
View File
@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate, Trans } from 'react-i18next';
import Raven from 'raven-js';
import Message from 'components/Message';
class RavenError extends React.Component {
componentDidMount() {
const { error, details } = this.props;
Raven.captureException(error, details);
}
reportError = event => {
event.preventDefault();
if (Raven.lastEventId()) {
Raven.showReportDialog();
}
}
render() {
const { t } = this.props;
return <Message type="error" heading={ t('An error has occurred') }>
<p><Trans i18nKey="This error has been logged">
This error has been logged. You may also <a href="#error-report" onClick={ this.reportError }>fill out a report</a>.
</Trans></p>
</Message>;
}
}
RavenError.propTypes = {
error: PropTypes.object.isRequired,
details: PropTypes.object.isRequired,
t: PropTypes.func
};
export default translate()(RavenError);
export { RavenError };
+65
View File
@@ -0,0 +1,65 @@
jest.mock('raven-js');
import React from 'react';
import { shallow } from 'enzyme';
import Raven from 'raven-js';
import { RavenError } from 'components/RavenError';
import { translate } from '__mocks__/i18n';
const testError = { error: 'test error' };
const testDetails = { details: 'test details' };
describe('RavenError', () => {
test('rendering', () => {
const component = shallow(
<RavenError
error={ testError }
details={ testDetails }
t={ translate }/>
);
expect(component).toMatchSnapshot();
});
test('captures exception', () => {
shallow(
<RavenError
error={ testError }
details={ testDetails }
t={ translate }/>
);
expect(Raven.captureException).toHaveBeenCalledWith(testError, testDetails);
});
describe('error reporting', () => {
test('clicking to fill out a report when an event has been logged', () => {
Raven.lastEventId.mockReturnValue(1);
const component = shallow(
<RavenError
error={ testError }
details={ testDetails }
t={ translate }/>
);
const eventObj = { preventDefault: jest.fn() };
component.find('a').simulate('click', eventObj);
expect(eventObj.preventDefault).toHaveBeenCalled();
expect(Raven.showReportDialog).toHaveBeenCalled();
});
test('clicking to fill out a report when an event has not been logged', () => {
Raven.lastEventId.mockReturnValue(false);
const component = shallow(
<RavenError
error={ testError }
details={ testDetails }
t={ translate }/>
);
const eventObj = { preventDefault: jest.fn() };
component.find('a').simulate('click', eventObj);
expect(eventObj.preventDefault).toHaveBeenCalled();
expect(Raven.showReportDialog).not.toHaveBeenCalled();
});
});
});
@@ -1,510 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render debugging 1`] = `
<DocumentFragment>
<div
class="render"
>
<svg
style="background-color: rgb(255, 255, 255);"
viewBox="0 0 "
xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
>
<metadata>
<rdf:rdf>
<cc:license
rdf:about="http://creativecommons.org/licenses/by/3.0/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:license>
</rdf:rdf>
</metadata>
<g
transform="translate(10 10)"
>
<span
data-component="Text"
data-props="{}"
>
Example
</span>
<rect
height="50"
style="fill: transparent; stroke: red; stroke-width: 1px; stroke-dasharray: 2,2; opacity: 0.5;"
width="100"
/>
<circle
cx="5"
cy="10"
r="3"
style="fill: red; opacity: 0.5;"
/>
<circle
cx="95"
cy="10"
r="3"
style="fill: red; opacity: 0.5;"
/>
</g>
</svg>
</div>
</DocumentFragment>
`;
exports[`Render types Box 1`] = `
<DocumentFragment>
<div
class="render"
>
<svg
style="background-color: rgb(255, 255, 255);"
viewBox="0 0 "
xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
>
<metadata>
<rdf:rdf>
<cc:license
rdf:about="http://creativecommons.org/licenses/by/3.0/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:license>
</rdf:rdf>
</metadata>
<g
transform="translate(10 10)"
>
<span
data-component="Box"
data-props="{}"
>
<span
data-component="Text"
data-props="{}"
>
Example
</span>
</span>
</g>
</svg>
</div>
</DocumentFragment>
`;
exports[`Render types HorizontalLayout 1`] = `
<DocumentFragment>
<div
class="render"
>
<svg
style="background-color: rgb(255, 255, 255);"
viewBox="0 0 "
xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
>
<metadata>
<rdf:rdf>
<cc:license
rdf:about="http://creativecommons.org/licenses/by/3.0/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:license>
</rdf:rdf>
</metadata>
<g
transform="translate(10 10)"
>
<span
data-component="HorizontalLayout"
data-props="{}"
>
<span
data-component="Text"
data-props="{}"
>
Example
</span>
<span
data-component="Text"
data-props="{}"
>
Another Example
</span>
</span>
</g>
</svg>
</div>
</DocumentFragment>
`;
exports[`Render types Loop 1`] = `
<DocumentFragment>
<div
class="render"
>
<svg
style="background-color: rgb(255, 255, 255);"
viewBox="0 0 "
xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
>
<metadata>
<rdf:rdf>
<cc:license
rdf:about="http://creativecommons.org/licenses/by/3.0/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:license>
</rdf:rdf>
</metadata>
<g
transform="translate(10 10)"
>
<span
data-component="Loop"
data-props="{}"
>
<span
data-component="Text"
data-props="{}"
>
Example
</span>
</span>
</g>
</svg>
</div>
</DocumentFragment>
`;
exports[`Render types Pin 1`] = `
<DocumentFragment>
<div
class="render"
>
<svg
style="background-color: rgb(255, 255, 255);"
viewBox="0 0 "
xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
>
<metadata>
<rdf:rdf>
<cc:license
rdf:about="http://creativecommons.org/licenses/by/3.0/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:license>
</rdf:rdf>
</metadata>
<g
transform="translate(10 10)"
>
<span
data-component="Pin"
data-props="{}"
/>
</g>
</svg>
</div>
</DocumentFragment>
`;
exports[`Render types Text 1`] = `
<DocumentFragment>
<div
class="render"
>
<svg
style="background-color: rgb(255, 255, 255);"
viewBox="0 0 "
xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
>
<metadata>
<rdf:rdf>
<cc:license
rdf:about="http://creativecommons.org/licenses/by/3.0/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:license>
</rdf:rdf>
</metadata>
<g
transform="translate(10 10)"
>
<span
data-component="Text"
data-props="{}"
>
Example
</span>
</g>
</svg>
</div>
</DocumentFragment>
`;
exports[`Render types VerticalLayout 1`] = `
<DocumentFragment>
<div
class="render"
>
<svg
style="background-color: rgb(255, 255, 255);"
viewBox="0 0 "
xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
>
<metadata>
<rdf:rdf>
<cc:license
rdf:about="http://creativecommons.org/licenses/by/3.0/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice"
/>
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:license>
</rdf:rdf>
</metadata>
<g
transform="translate(10 10)"
>
<span
data-component="VerticalLayout"
data-props="{}"
>
<span
data-component="Text"
data-props="{}"
>
Example
</span>
<span
data-component="Text"
data-props="{}"
>
Another Example
</span>
</span>
</g>
</svg>
</div>
</DocumentFragment>
`;
-79
View File
@@ -1,79 +0,0 @@
import React, { useRef, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import nodeTypes from 'rendering/types';
import style from './style.module.css';
const debugBox = {
fill: 'transparent',
stroke: 'red',
strokeWidth: '1px',
strokeDasharray: '2,2',
opacity: 0.5
};
const debugPin = {
fill: 'red',
opacity: 0.5
};
// eslint-disable-next-line react/prop-types
const renderDebug = ({ x, y, width, height, axisX1, axisX2, axisY }) => <>
<rect style={ debugBox } x={ x } y={ y } width={ width } height={ height }/>
<circle style={ debugPin } cx={ axisX1 } cy={ axisY } r="3" />
<circle style={ debugPin } cx={ axisX2 } cy={ axisY } r="3" />
</>;
const render = (data, key) => {
if (typeof data === 'string') {
return data;
}
const { type, props, debug, box } = data;
const children = (data.children || []).map(render);
return <React.Fragment key={ key }>
{ React.createElement(
nodeTypes[type] ? nodeTypes[type].default : type,
props,
children.length === 1 ? children[0] : children) }
{ debug && renderDebug(box) }
</React.Fragment>;
};
const Render = ({ data, onRender }) => {
const svgContainer = useRef();
const provideSVGData = useCallback(() => {
if (!svgContainer.current) {
return;
}
const svg = svgContainer.current.querySelector('svg');
onRender({
svg: svg.outerHTML,
width: Number(svg.getAttribute('width')),
height: Number(svg.getAttribute('height'))
});
}, [svgContainer, onRender]);
useEffect(() => {
provideSVGData();
}, [provideSVGData]);
return <div className={ style.render } ref={ svgContainer }>
{ render({
...data,
props: {
...data.props,
onReflow: provideSVGData
}
}) }
</div>;
};
Render.propTypes = {
data: PropTypes.object.isRequired,
onRender: PropTypes.func.isRequired
};
export default Render;
-15
View File
@@ -1,15 +0,0 @@
@import url('../../globals.module.css');
.render {
width: 100%;
background: var(--color-white);
box-sizing: border-box;
overflow: auto;
margin: var(--spacing-margin) 0;
& svg {
display: block;
transform: scaleZ(1); /* Move to separate render layer in Chrome */
margin: 0 auto;
}
}
-128
View File
@@ -1,128 +0,0 @@
jest.mock('rendering/Box', () =>
require('__mocks__/component-mock')('rendering/Box'));
jest.mock('rendering/HorizontalLayout', () =>
require('__mocks__/component-mock')('rendering/HorizontalLayout'));
jest.mock('rendering/Loop', () =>
require('__mocks__/component-mock')('rendering/Loop'));
jest.mock('rendering/Pin', () =>
require('__mocks__/component-mock')('rendering/Pin'));
jest.mock('rendering/Text', () =>
require('__mocks__/component-mock')('rendering/Text'));
jest.mock('rendering/VerticalLayout', () =>
require('__mocks__/component-mock')('rendering/VerticalLayout'));
import React from 'react';
import { render } from 'react-testing-library';
import Render from 'components/Render';
const testType = (name, item) => {
test(name, () => {
const data = { type: 'SVG', children: [item] };
const { asFragment } = render(
<Render data={ data } onRender={ jest.fn() }/>
);
expect(asFragment()).toMatchSnapshot();
});
};
describe('Render', () => {
test('debugging', () => {
const data = {
type: 'SVG',
children: [
{
type: 'Text',
debug: true,
box: {
width: 100,
height: 50,
axisY: 10,
axisX1: 5,
axisX2: 95
},
children: [
'Example'
]
}
]
};
const { asFragment } = render(
<Render data={ data } onRender={ jest.fn() }/>
);
expect(asFragment()).toMatchSnapshot();
});
describe('types', () => {
testType('Pin', {
type: 'Pin'
});
testType('Text', {
type: 'Text',
children: [
'Example'
]
});
testType('Box', {
type: 'Box',
children: [
{
type: 'Text',
children: [
'Example'
]
}
]
});
testType('Loop', {
type: 'Loop',
children: [
{
type: 'Text',
children: [
'Example'
]
}
]
});
testType('HorizontalLayout', {
type: 'HorizontalLayout',
children: [
{
type: 'Text',
children: [
'Example'
]
},
{
type: 'Text',
children: [
'Another Example'
]
}
]
});
testType('VerticalLayout', {
type: 'VerticalLayout',
children: [
{
type: 'Text',
children: [
'Example'
]
},
{
type: 'Text',
children: [
'Another Example'
]
}
]
});
});
});
+80
View File
@@ -0,0 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './style';
import reflowable from './reflowable';
@reflowable
class Box extends React.PureComponent {
static defaultProps = {
padding: 5,
radius: 3
}
label = React.createRef()
children = [React.createRef()]
reflow() {
const { padding, useAnchors } = this.props;
const box = this.children[0].current.getBBox();
const labelBox = this.label.current ? this.label.current.getBBox() : { width: 0, height: 0};
this.setBBox({
width: Math.max(box.width + 2 * padding, labelBox.width),
height: box.height + 2 * padding + labelBox.height,
axisY: (useAnchors ? box.axisY : box.height / 2) + padding + labelBox.height,
axisX1: useAnchors ? box.axisX1 + padding : 0,
axisX2: useAnchors ? box.axisX2 + padding : box.width + 2 * padding
});
this.setStateAsync({
width: this.getBBox().width,
height: box.height + 2 * padding,
contentTransform: `translate(${ padding } ${ padding + labelBox.height })`,
rectTransform: `translate(0 ${ labelBox.height })`,
labelTransform: `translate(0 ${ labelBox.height })`
});
}
render() {
const { theme, radius, label, children } = this.props;
const { width, height, labelTransform, rectTransform, contentTransform } = this.state || {};
const rectProps = {
style: style[theme],
width,
height,
rx: radius,
ry: radius,
transform: rectTransform
};
const textProps = {
transform: labelTransform,
style: style.infoText,
ref: this.label
};
return <React.Fragment>
<rect { ...rectProps } ></rect>
{ label && <text { ...textProps }>{ label }</text> }
<g transform={ contentTransform }>
{ React.cloneElement(React.Children.only(children), {
ref: this.children[0]
}) }
</g>
</React.Fragment>;
}
}
Box.propTypes = {
children: PropTypes.node,
label: PropTypes.string,
padding: PropTypes.number,
useAnchors: PropTypes.bool,
radius: PropTypes.number,
theme: PropTypes.string
};
export default Box;
+53
View File
@@ -0,0 +1,53 @@
import React from 'react';
import { mount } from 'enzyme';
import Box from './Box';
import SVGElement from '__mocks__/SVGElement';
const originalGetBBox = window.Element.prototype.getBBox;
describe('Box', () => {
beforeEach(() => {
window.Element.prototype.getBBox = function() {
return { width: 100, height: 10 };
};
});
afterEach(() => {
window.Element.prototype.getBBox = originalGetBBox;
});
test('rendering', async () => {
const component = mount(
<Box>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Box>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with content anchors', async () => {
const component = mount(
<Box useAnchors>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Box>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with label', async () => {
const component = mount(
<Box label="Test label">
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Box>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
});
+97
View File
@@ -0,0 +1,97 @@
import React from 'react';
import PropTypes from 'prop-types';
import { List } from 'immutable';
import style from './style';
import reflowable from './reflowable';
import Path from './path';
@reflowable
class HorizontalLayout extends React.PureComponent {
static defaultProps = {
withConnectors: false,
spacing: 10
}
state = {
childTransforms: List()
}
children = []
updateChildTransforms(childBoxes) {
return this.state.childTransforms.withMutations(transforms => (
childBoxes.forEach((box, i) => (
transforms.set(i, `translate(${ box.offsetX } ${ box.offsetY })`)
))
));
}
updateConnectorPaths(childBoxes) {
let last = childBoxes[0];
return childBoxes.slice(1).reduce((path, box) => {
try {
return path
.moveTo({ x: last.offsetX + last.axisX2, y: this.getBBox().axisY })
.lineTo({ x: box.offsetX + box.axisX1 });
}
finally {
last = box;
}
}, new Path()).toString();
}
reflow() {
const { spacing, withConnectors } = this.props;
const childBoxes = this.children.map(child => child.current.getBBox());
const verticalCenter = childBoxes.reduce((center, box) => Math.max(center, box.axisY), 0);
const width = childBoxes.reduce((width, box) => width + box.width, 0) + (childBoxes.length - 1) * spacing;
const height = childBoxes.reduce((ascHeight, box) => Math.max(ascHeight, box.axisY), 0) +
childBoxes.reduce((decHeight, box) => Math.max(decHeight, box.height - box.axisY), 0);
this.setBBox({ width, height, axisY: verticalCenter }, { axisX1: true, axisX2: true });
let offset = 0;
childBoxes.forEach(box => {
box.offsetX = offset;
box.offsetY = this.getBBox().axisY - box.axisY;
offset += box.width + spacing;
});
this.setStateAsync({
childTransforms: this.updateChildTransforms(childBoxes),
connectorPaths: withConnectors ? this.updateConnectorPaths(childBoxes) : ''
});
}
render() {
const { children } = this.props;
const { childTransforms, connectorPaths } = this.state;
this.makeRefCollection(this.children, React.Children.count(children));
return <React.Fragment>
<path d={ connectorPaths } style={ style.connectors }></path>
{ React.Children.map(children, (child, i) => (
<g transform={ childTransforms.get(i) }>
{ React.cloneElement(child, {
ref: this.children[i]
}) }
</g>
))}
</React.Fragment>;
}
}
HorizontalLayout.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
spacing: PropTypes.number,
withConnectors: PropTypes.bool
};
export default HorizontalLayout;
@@ -0,0 +1,34 @@
import React from 'react';
import { mount } from 'enzyme';
import HorizontalLayout from './HorizontalLayout';
import SVGElement from '__mocks__/SVGElement';
describe('HorizontalLayout', () => {
test('rendering', async () => {
const component = mount(
<HorizontalLayout>
<SVGElement bbox={{ width: 100, height: 100 }}/>
<SVGElement bbox={{ width: 100, height: 100 }}/>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</HorizontalLayout>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with connectors', async () => {
const component = mount(
<HorizontalLayout withConnectors>
<SVGElement bbox={{ width: 100, height: 100 }}/>
<SVGElement bbox={{ width: 100, height: 100 }}/>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</HorizontalLayout>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
});
+100
View File
@@ -0,0 +1,100 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './style';
import reflowable from './reflowable';
const namespaceProps = {
'xmlns': 'http://www.w3.org/2000/svg',
'xmlns:cc': 'http://creativecommons.org/ns#',
'xmlns:rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'
};
const metadata = `<rdf:rdf>
<cc:license rdf:about="http://creativecommons.org/licenses/by/3.0/">
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"></cc:permits>
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"></cc:permits>
<cc:requires rdf:resource="http://creativecommons.org/ns#Notice"></cc:requires>
<cc:requires rdf:resource="http://creativecommons.org/ns#Attribution"></cc:requires>
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"></cc:permits>
</cc:license>
</rdf:rdf>`;
@reflowable
class Image extends React.PureComponent {
static defaultProps = {
padding: 10
}
state = {
width: 0,
height: 0
}
svg = React.createRef()
children = [React.createRef()]
async svgUrl(type) {
const markup = this.svg.current.outerHTML;
return new Blob([markup], { type });
}
async pngUrl(type) {
const markup = this.svg.current.outerHTML;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const loader = new window.Image(); // Using window.Image to avoid name conflict :(
loader.width = canvas.width = Number(this.svg.current.getAttribute('width')) * 2;
loader.height = canvas.height = Number(this.svg.current.getAttribute('height')) * 2;
await new Promise(resolve => {
loader.onload = resolve;
loader.src = 'data:image/svg+xml,' + encodeURIComponent(markup);
});
context.drawImage(loader, 0, 0, loader.width, loader.height);
return new Promise(resolve => canvas.toBlob(resolve, type));
}
reflow() {
const { padding } = this.props;
const box = this.children[0].current.getBBox();
this.setStateAsync({
width: Math.round(box.width + 2 * padding),
height: Math.round(box.height + 2 * padding)
});
}
render() {
const { width, height } = this.state;
const { padding, children } = this.props;
const svgProps = {
width,
height,
viewBox: [0, 0, width, height].join(' '),
style: style.image,
ref: this.svg,
...namespaceProps
};
return <svg { ...svgProps }>
<metadata dangerouslySetInnerHTML={{ __html: metadata }}></metadata>
<g transform={ `translate(${ padding } ${ padding })` }>
{ React.cloneElement(React.Children.only(children), {
ref: this.children[0]
}) }
</g>
</svg>;
}
}
Image.propTypes = {
children: PropTypes.node,
padding: PropTypes.number
};
export default Image;
+19
View File
@@ -0,0 +1,19 @@
import React from 'react';
import { mount } from 'enzyme';
import Image from './Image';
import SVGElement from '__mocks__/SVGElement';
describe('Image', () => {
test('rendering', async () => {
const component = mount(
<Image>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Image>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
});
+139
View File
@@ -0,0 +1,139 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './style';
import reflowable from './reflowable';
import Path from './path';
const skipPath = (box, greedy) => {
const vert = Math.max(0, box.axisY - 10);
const horiz = box.width - 10;
let path = new Path({ relative: true });
if (!greedy) {
path
.moveTo({ x: 10, y: box.axisY + box.offsetY - 15, relative: false })
.lineTo({ x: 5, y: 5 })
.moveTo({ x: -5, y: -5 })
.lineTo({ x: -5, y: 5 });
}
return path
.moveTo({ x: 0, y: box.axisY + box.offsetY, relative: false })
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: -10 })
.lineTo({ y: -vert })
.quadraticCurveTo({ cx: 0, cy: -10, x: 10, y: -10 })
.lineTo({ x: horiz })
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: 10 })
.lineTo({ y: vert })
.quadraticCurveTo({ cx: 0, cy: 10, x: 10, y: 10 });
};
const repeatPath = (box, greedy) => {
const vert = box.height - box.axisY - 10;
let path = new Path({ relative: true });
if (greedy) {
path
.moveTo({ x: box.offsetX + box.width + 10, y: box.axisY + box.offsetY + 15, relative: false })
.lineTo({ x: 5, y: -5 })
.moveTo({ x: -5, y: 5 })
.lineTo({ x: -5, y: -5 });
}
return path
.moveTo({ x: box.offsetX, y: box.axisY + box.offsetY, relative: false })
.quadraticCurveTo({ cx: -10, cy: 0, x: -10, y: 10 })
.lineTo({ y: vert })
.quadraticCurveTo({ cx: 0, cy: 10, x: 10, y: 10 })
.lineTo({ x: box.width })
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: -10 })
.lineTo({ y: -vert })
.quadraticCurveTo({ cx: 0, cy: -10, x: -10, y: -10 });
};
@reflowable
class Loop extends React.PureComponent {
label = React.createRef()
children = [React.createRef()]
get contentOffset() {
const { skip, repeat } = this.props;
if (skip) {
return { x: 15, y: 10 };
} else if (repeat) {
return { x: 10, y: 0 };
} else {
return { x: 0, y: 0 };
}
}
reflow() {
const { skip, repeat, greedy } = this.props;
const box = this.children[0].current.getBBox();
const labelBox = this.label.current ? this.label.current.getBBox() : { width: 0, height: 0 };
let height = box.height + labelBox.height;
if (skip) {
height += 10;
}
if (repeat) {
height += 10;
}
this.setBBox({
width: box.width + this.contentOffset.x * 2,
height,
axisY: box.axisY + this.contentOffset.y,
axisX1: box.axisX1 + this.contentOffset.x,
axisX2: box.axisX2 + this.contentOffset.x
});
box.offsetX = this.contentOffset.x;
box.offsetY = this.contentOffset.y;
this.setStateAsync({
labelTransform: `translate(${ this.getBBox().width - labelBox.width - 10 } ${ this.getBBox().height + 2 })`,
loopPaths: [
skip && skipPath(box, greedy),
repeat && repeatPath(box, greedy)
].filter(Boolean).join('')
});
}
render() {
const { label, children } = this.props;
const { loopPaths, labelTransform } = this.state || {};
const textProps = {
transform: labelTransform,
style: style.infoText,
ref: this.label
};
return <React.Fragment>
<path d={ loopPaths } style={ style.connectors }></path>
{ label && <text { ...textProps }>{ label }</text> }
<g transform={ `translate(${ this.contentOffset.x } ${ this.contentOffset.y })` }>
{ React.cloneElement(React.Children.only(children), {
ref: this.children[0]
}) }
</g>
</React.Fragment>;
}
}
Loop.propTypes = {
children: PropTypes.node.isRequired,
greedy: PropTypes.bool,
label: PropTypes.string,
skip: PropTypes.bool,
repeat: PropTypes.bool
};
export default Loop;
+86
View File
@@ -0,0 +1,86 @@
import React from 'react';
import { mount } from 'enzyme';
import Loop from './Loop';
import SVGElement from '__mocks__/SVGElement';
const originalGetBBox = window.Element.prototype.getBBox;
describe('Loop', () => {
beforeEach(() => {
window.Element.prototype.getBBox = function() {
return { width: 100, height: 10 };
};
});
afterEach(() => {
window.Element.prototype.getBBox = originalGetBBox;
});
test('rendering', async () => {
const component = mount(
<Loop>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with skip path', async () => {
const component = mount(
<Loop skip>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with repeat path', async () => {
const component = mount(
<Loop repeat>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with repeat path and label', async () => {
const component = mount(
<Loop repeat label="Test label">
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with skip and repeat paths', async () => {
const component = mount(
<Loop skip repeat>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with greedy skip and repeat paths', async () => {
const component = mount(
<Loop greedy skip repeat>
<SVGElement bbox={{ width: 100, height: 100 }}/>
</Loop>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
});
+40
View File
@@ -0,0 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './style';
import reflowable from './reflowable';
@reflowable
class Pin extends React.PureComponent {
static defaultProps = {
radius: 5
}
reflow() {
const { radius } = this.props;
this.setBBox({
width: radius * 2,
height: radius * 2
});
}
render() {
const { radius } = this.props;
const circleProps = {
r: radius,
style: style.pin,
transform: `translate(${ radius } ${ radius })`
};
return <circle { ...circleProps }></circle>;
}
}
Pin.propTypes = {
radius: PropTypes.number
};
export default Pin;
+15
View File
@@ -0,0 +1,15 @@
import React from 'react';
import { mount } from 'enzyme';
import Pin from './Pin';
describe('Pin', () => {
test('rendering', async () => {
const component = mount(
<Pin/>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
});
+63
View File
@@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import style from './style';
import reflowable from './reflowable';
@reflowable
class Text extends React.PureComponent {
text = React.createRef()
reflow() {
const box = this.text.current.getBBox();
this.setBBox({
width: box.width,
height: box.height
});
this.setStateAsync({
transform: `translate(${-box.x} ${-box.y})`
});
}
renderContent() {
const { children, quoted } = this.props;
if (!quoted) {
return children;
}
return <React.Fragment>
<tspan style={ style.textQuote }>&ldquo;</tspan>
<tspan>{ children }</tspan>
<tspan style={ style.textQuote }>&rdquo;</tspan>
</React.Fragment>;
}
render() {
const { theme } = this.props;
const { transform } = this.state || {};
const textProps = {
style: { ...style.text, ...style[theme] },
transform,
ref: this.text
};
return <text { ...textProps }>
{ this.renderContent() }
</text>;
}
}
Text.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
quoted: PropTypes.bool,
theme: PropTypes.string
};
export default Text;
+36
View File
@@ -0,0 +1,36 @@
import React from 'react';
import { mount } from 'enzyme';
import Text from './Text';
const originalGetBBox = window.Element.prototype.getBBox;
describe('Text', () => {
beforeEach(() => {
window.Element.prototype.getBBox = function() {
return { x: 10, y: 10 };
};
});
afterEach(() => {
window.Element.prototype.getBBox = originalGetBBox;
});
test('rendering', async () => {
const component = mount(
<Text>Test content</Text>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
test('rendering with quotes', async () => {
const component = mount(
<Text quoted>Test content</Text>
);
await component.instance().doReflow();
component.update();
expect(component).toMatchSnapshot();
});
});
+139
View File
@@ -0,0 +1,139 @@
import React from 'react';
import PropTypes from 'prop-types';
import { List } from 'immutable';
import style from './style';
import reflowable from './reflowable';
import Path from './path';
const connectorMargin = 20;
@reflowable
class VerticalLayout extends React.PureComponent {
static defaultProps = {
withConnectors: false,
spacing: 10
}
state = {
childTransforms: List()
}
children = []
updateChildTransforms(childBoxes) {
return this.state.childTransforms.withMutations(transforms => (
childBoxes.forEach((box, i) => (
transforms.set(i, `translate(${ box.offsetX } ${ box.offsetY })`)
))
));
}
makeCurve(box) {
const thisBox = this.getBBox();
const distance = Math.abs(box.offsetY + box.axisY - thisBox.axisY);
if (distance >= 15) {
const curve = (box.axisY + box.offsetY > thisBox.axisY) ? 10 : -10;
return new Path()
// Left
.moveTo({ x: 10, y: box.axisY + box.offsetY - curve })
.quadraticCurveTo({ cx: 0, cy: curve, x: 10, y: curve, relative: true })
.lineTo({ x: box.offsetX + box.axisX1 })
// Right
.moveTo({ x: thisBox.width - 10, y: box.axisY + box.offsetY - curve })
.quadraticCurveTo({ cx: 0, cy: curve, x: -10, y: curve, relative: true })
.lineTo({ x: box.offsetX + box.axisX2 });
} else {
const anchor = box.offsetY + box.axisY - thisBox.axisY;
return new Path()
// Left
.moveTo({ x: 0, y: thisBox.axisY })
.cubicCurveTo({ cx1: 15, cy1: 0, cx2: 10, cy2: anchor, x: 20, y: anchor, relative: true })
.lineTo({ x: box.offsetX + box.axisX1 })
// Right
.moveTo({ x: thisBox.width, y: thisBox.axisY })
.cubicCurveTo({ cx1: -15, cy1: 0, cx2: -10, cy2: anchor, x: -20, y: anchor, relative: true })
.lineTo({ x: box.offsetX + box.axisX2 });
}
}
makeSide(box) {
const thisBox = this.getBBox();
const distance = Math.abs(box.offsetY + box.axisY - thisBox.axisY);
if (distance >= 15) {
const shift = (box.offsetY + box.axisY > thisBox.axisY) ? 10 : -10;
const edge = box.offsetY + box.axisY - shift;
return new Path()
// Left
.moveTo({ x: 0, y: thisBox.axisY })
.quadraticCurveTo({ cx: 10, cy: 0, x: 10, y: shift, relative: true })
.lineTo({ y: edge })
// Right
.moveTo({ x: thisBox.width, y: thisBox.axisY })
.quadraticCurveTo({ cx: -10, cy: 0, x: -10, y: shift, relative: true })
.lineTo({ y: edge });
}
}
reflow() {
const { spacing, withConnectors } = this.props;
const childBoxes = this.children.map(child => child.current.getBBox());
const horizontalCenter = childBoxes.reduce((center, box) => Math.max(center, box.width / 2), 0);
const margin = withConnectors ? connectorMargin : 0;
const width = childBoxes.reduce((width, box) => Math.max(width, box.width), 0) + 2 * margin;
const height = childBoxes.reduce((height, box) => height + box.height, 0) + (childBoxes.length - 1) * spacing;
this.setBBox({ width, height }, { axisY: true, axisX1: true, axisX2: true });
let offset = 0;
childBoxes.forEach(box => {
box.offsetX = horizontalCenter - box.width / 2 + margin;
box.offsetY = offset;
offset += spacing + box.height;
});
this.setStateAsync({
childTransforms: this.updateChildTransforms(childBoxes),
connectorPaths: withConnectors ? [
...childBoxes.map(box => this.makeCurve(box)),
this.makeSide(childBoxes[0]),
this.makeSide(childBoxes[childBoxes.length - 1])
].join('') : ''
});
}
render() {
const { children } = this.props;
const { childTransforms, connectorPaths } = this.state;
this.makeRefCollection(this.children, React.Children.count(children));
return <React.Fragment>
<path d={ connectorPaths } style={ style.connectors }></path>
{ React.Children.map(children, (child, i) => (
<g transform={ childTransforms.get(i) }>
{ React.cloneElement(child, {
ref: this.children[i]
}) }
</g>
)) }
</React.Fragment>;
}
}
VerticalLayout.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
spacing: PropTypes.number,
withConnectors: PropTypes.bool
};
export default VerticalLayout;

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