Compare commits

..

309 Commits

Author SHA1 Message Date
Alejandro Celaya
d231ed3ede Merge pull request #247 from acelaya-forks/feature/user-agent-improvements
Feature/user agent improvements
2020-04-10 20:06:57 +02:00
Alejandro Celaya
cf6f9028f2 Some more improvements on how chart height is calculated 2020-04-10 19:57:33 +02:00
Alejandro Celaya
7cf49d2c1a Increased minimum charts height 2020-04-10 19:47:42 +02:00
Alejandro Celaya
e37fb1b4bd Updated changelog 2020-04-10 19:29:57 +02:00
Alejandro Celaya
faf5d0bf7b Unified function parsing user agent for browser and os 2020-04-10 19:22:13 +02:00
Alejandro Celaya
6fede88072 Added dependency on bowser to have a more accurate browser and OS detection 2020-04-10 19:16:44 +02:00
Alejandro Celaya
87ffbefa61 Merge pull request #246 from acelaya-forks/feature/create-improvements
Feature/create improvements
2020-04-10 18:50:09 +02:00
Alejandro Celaya
f33ae17781 Updated changelog 2020-04-10 18:43:16 +02:00
Alejandro Celaya
2a2bae6d1a Improved short URL creation 2020-04-10 18:42:08 +02:00
Alejandro Celaya
eb65e99024 Merge pull request #244 from acelaya-forks/feature/chart-visit-highlighting
Feature/chart visit highlighting
2020-04-10 15:21:07 +02:00
Alejandro Celaya
52dbeb6201 Optimized visits parser to act over the normalized list of visits 2020-04-10 14:59:12 +02:00
Alejandro Celaya
fafe920b7b Ensured highlighted stats are properly sorted and paginated on charts that support that 2020-04-10 14:38:31 +02:00
Alejandro Celaya
9d1e48ee90 Updated main list paginator to be sticky 2020-04-10 13:42:21 +02:00
Alejandro Celaya
3851342e1b Added button to reset visits selection 2020-04-10 13:27:01 +02:00
Alejandro Celaya
b863c2e19d Used cursor pointer in bar charts 2020-04-10 13:04:39 +02:00
Alejandro Celaya
ed584d19e5 Ensured charts datasets have a unique label 2020-04-10 12:57:14 +02:00
Alejandro Celaya
73256dcf5b Handled toggling between highlighted chart bars 2020-04-10 12:53:54 +02:00
Alejandro Celaya
c67a23c988 Added support to disable date inputs 2020-04-10 12:25:06 +02:00
Alejandro Celaya
8f42e65ccd Allowed visits to be selected on charts so that they get highlighted on the rest of the charts 2020-04-10 11:59:53 +02:00
Alejandro Celaya
05deb1aff0 Merge pull request #242 from acelaya-forks/feature/visits-table
Feature/visits table
2020-04-09 11:23:51 +02:00
Alejandro Celaya
a74b7cdfad Updated changelog 2020-04-09 11:00:27 +02:00
Alejandro Celaya
1c3119ee76 Allowed multiple selection on visits table 2020-04-09 10:56:54 +02:00
Alejandro Celaya
ca52911e42 Added VisitsTable test 2020-04-09 10:21:38 +02:00
Alejandro Celaya
9177bc7cef Tested how hilghlighted data behaves on GraphCards 2020-04-09 09:44:14 +02:00
Alejandro Celaya
310831a26a Converted ShortUrlVisits in functional component 2020-04-07 22:33:41 +02:00
Alejandro Celaya
8a486d991b Implemented some improvements and fixes on how visits table is split and calculated 2020-04-05 18:04:15 +02:00
Alejandro Celaya
b79333393b Converted SearchField component into funcitonal component 2020-04-05 16:18:08 +02:00
Alejandro Celaya
cb7062bb95 Created fake border with before and after pseudoelements for sticky table cells 2020-04-05 16:02:42 +02:00
Alejandro Celaya
94c5b2c471 Improved useToggle hook so that it also returns enabler and disabler 2020-04-05 12:18:41 +02:00
Alejandro Celaya
66bf26f1dc Improved highlighted data calculation so that it works with values different than 1 2020-04-05 11:57:39 +02:00
Alejandro Celaya
f5cc1abe75 Ensured info for selected visit in visits table gets highlighted in bar charts 2020-04-04 20:16:20 +02:00
Alejandro Celaya
bd4255108d Improved VisitsTable performance by memoizing visits lists 2020-04-04 12:58:04 +02:00
Alejandro Celaya
06b63d1af2 Improved rendering of visits table on mobile devices 2020-04-04 12:09:17 +02:00
Alejandro Celaya
2bd70fb9e6 Fixed unit tests 2020-04-04 10:36:38 +02:00
Alejandro Celaya
e6034dfb14 Created VisitsTable 2020-04-03 23:00:57 +02:00
Alejandro Celaya
c8ba6764c2 Merge pull request #238 from acelaya-forks/feature/edit-long-url
Feature/edit long url
2020-03-30 21:36:20 +02:00
Alejandro Celaya
19337d6c05 Added tests for elements regarding short URL edition 2020-03-30 21:26:30 +02:00
Alejandro Celaya
a6ad3c2d4d Updated changelog 2020-03-30 21:01:54 +02:00
Alejandro Celaya
b0dd885c09 Converted ShortUrlsRowMenu into functional component 2020-03-30 21:01:01 +02:00
Alejandro Celaya
2235592308 Fixed ShortUrlsRowMenu test 2020-03-30 20:50:31 +02:00
Alejandro Celaya
1219a16261 Ensured short URLs list is updated after editing the long URL of a short URL 2020-03-30 20:47:33 +02:00
Alejandro Celaya
7949e224e0 Created modal to edit the loing URL behind a short URL 2020-03-30 20:42:58 +02:00
Alejandro Celaya
ab2f311bb7 Merge pull request #237 from acelaya-forks/feature/short-code-length
Feature/short code length
2020-03-29 19:49:09 +02:00
Alejandro Celaya
a5aab43666 Updated changelog 2020-03-29 19:41:29 +02:00
Alejandro Celaya
74ebd4e572 Converted CreateShortUrl to functional component 2020-03-29 19:36:45 +02:00
Alejandro Celaya
bd29670108 Added short code length field to form to create short URLs 2020-03-29 18:55:41 +02:00
Alejandro Celaya
9a20b4428d Merge pull request #236 from acelaya-forks/feature/progressive-paginator
Feature/progressive paginator
2020-03-28 17:52:35 +01:00
Alejandro Celaya
d7da8521ce Created helper functions to determine the key and if a page is disabled on a progressive paginator 2020-03-28 17:43:09 +01:00
Alejandro Celaya
bab3b252c1 Updated changelog 2020-03-28 17:35:02 +01:00
Alejandro Celaya
7f05c5c2da Split utils module into several helpers modules 2020-03-28 17:33:27 +01:00
Alejandro Celaya
2d5c2779c3 Moved helper functions to render progressive paginators to a common place 2020-03-28 17:25:12 +01:00
Alejandro Celaya
06db4f6556 Used progressive pagination for the short URLs list 2020-03-28 17:19:33 +01:00
Alejandro Celaya
ea5ec63a22 Ensured all branches build the docker image 2020-03-22 09:34:24 +01:00
Alejandro Celaya
f46e737e77 Merge pull request #233 from acelaya-forks/feature/fix-docker-build-condition
Fixed docker build condition so that it's run for any branch or tag a…
2020-03-21 13:39:31 +01:00
Alejandro Celaya
6e63bdaafa Fixed docker build condition so that it's run for any branch or tag as long as it is not a PR 2020-03-21 08:43:35 +01:00
Alejandro Celaya
79ccef9f7e Avoid latest docker to be build when building a tag 2020-03-21 06:59:37 +01:00
Alejandro Celaya
a9653b3674 Merge pull request #232 from acelaya-forks/feature/improve-docker-build
Feature/improve docker build
2020-03-20 09:23:35 +01:00
Alejandro Celaya
b5a188e802 Improved building process so that already generated dist files are reused when building docker image is possible 2020-03-20 09:12:43 +01:00
Alejandro Celaya
38fc402b16 Improved docker build script to avoid duplicating code 2020-03-20 07:12:07 +01:00
Alejandro Celaya
584d1ec1ce Fixed conditional in docker build script 2020-03-19 20:39:34 +01:00
Alejandro Celaya
2ca7faa457 Merge pull request #230 from acelaya-forks/feature/travis-docker-build
Added docker image building as a deployment step for travis
2020-03-19 20:32:00 +01:00
Alejandro Celaya
03806abda0 Changed build steps so that mutation testing a docker build are only run on pull request builds 2020-03-19 20:26:35 +01:00
Alejandro Celaya
18d125430d Added docker image building as a deployment step for travis 2020-03-19 20:04:30 +01:00
Alejandro Celaya
f57f6b7745 Merge pull request #228 from acelaya-forks/feature/memoize-server-version
Feature/memoize server version
2020-03-16 19:01:33 +01:00
Alejandro Celaya
75ff2b8f40 Added app gif to readme 2020-03-16 18:53:06 +01:00
Alejandro Celaya
2ec04c0121 Fixed test by using different serverId every time, preventing memoization 2020-03-16 18:51:04 +01:00
Alejandro Celaya
5145a41dac Memoized the loading of the server version, assuming it will not change at runtime 2020-03-16 13:34:24 +01:00
Alejandro Celaya
25c67f1c3e Merge pull request #227 from acelaya-forks/feature/edit-servers
Feature/edit servers
2020-03-15 14:32:30 +01:00
Alejandro Celaya
77b9181150 Replaced hardcoded color by sass var 2020-03-15 14:23:57 +01:00
Alejandro Celaya
e4f7ded8e2 Updated changelog 2020-03-15 14:04:33 +01:00
Alejandro Celaya
35a62f1fb1 Added link to edit existing servers 2020-03-15 14:03:41 +01:00
Alejandro Celaya
24f2deda46 Moved common code to handle currently selected server to HOC 2020-03-15 13:43:12 +01:00
Alejandro Celaya
5d8af1a0e5 Simplified EditServer component by wrapping ServerForm 2020-03-15 12:02:19 +01:00
Alejandro Celaya
6d44ac1e0c Created common component that can be used both for create and edit servers 2020-03-15 11:59:07 +01:00
Alejandro Celaya
fb0ebddf28 Created component to edit existing servers 2020-03-15 11:29:20 +01:00
Alejandro Celaya
0aebaa4da1 Extracted logic to render horizontal form groups to their own components 2020-03-15 10:50:05 +01:00
Alejandro Celaya
f6baedc655 Converted CreateServer into functional component 2020-03-15 10:33:23 +01:00
Alejandro Celaya
7db222664d Fixed tests 2020-03-15 09:56:16 +01:00
Alejandro Celaya
8223f0fd64 Undone weird changes in package lock file 2020-03-15 09:43:42 +01:00
Alejandro Celaya
f44ec42f51 Added links to delete and edit the server when a server could not be reached 2020-03-15 09:17:33 +01:00
Alejandro Celaya
dab75ab6a9 Updated badges 2020-03-10 21:53:21 +01:00
Alejandro Celaya
01672b88e1 Merge pull request #222 from acelaya-forks/feature/server-not-found
Feature/server not found
2020-03-08 13:17:56 +01:00
Alejandro Celaya
78dc297022 Updated changelog 2020-03-08 13:05:15 +01:00
Alejandro Celaya
c8cf75fa28 Created ServerError test 2020-03-08 13:04:21 +01:00
Alejandro Celaya
b011b4e1d8 Fixed tests 2020-03-08 12:57:01 +01:00
Alejandro Celaya
9804a2d18d Added list of servers connected to store in ServerError component 2020-03-08 12:50:42 +01:00
Alejandro Celaya
d1a5ee43e9 Created components to display errors when loading a server 2020-03-08 12:41:18 +01:00
Alejandro Celaya
febecab33c Migrated Home component to a functional component 2020-03-08 11:35:06 +01:00
Alejandro Celaya
99042c0979 Extracted servers list group from home component to a reusable component 2020-03-08 11:16:57 +01:00
Alejandro Celaya
6395e4e00b Improved NotFount component so that link text is passed as children 2020-03-08 10:28:04 +01:00
Alejandro Celaya
4a69907ca3 Fixed generation of component keys to make them render properly 2020-03-08 10:16:45 +01:00
Alejandro Celaya
c8d682cc98 Handled loading server in just one place, and added error handling for loading servers 2020-03-08 10:00:25 +01:00
Alejandro Celaya
f4cc8d3a0c Fixed default value for vertically aligned items 2020-03-07 12:07:51 +01:00
Alejandro Celaya
6ac89334fd Merge pull request #220 from acelaya-forks/feature/improvements
Feature/improvements
2020-03-06 21:56:20 +01:00
Alejandro Celaya
f55d3a66aa Converted ShortUrlsRow component into a functional component 2020-03-06 21:44:03 +01:00
Alejandro Celaya
972eafab34 Updated changelog 2020-03-06 21:26:19 +01:00
Alejandro Celaya
fba156b271 Moved copy-to-clipboard control next to short URL 2020-03-06 21:25:30 +01:00
Alejandro Celaya
96d538db15 Replaced Unknown by Direct for traffic comming from undetermined referrers 2020-03-06 20:42:22 +01:00
Alejandro Celaya
b89bfa3c1c Merge pull request #215 from acelaya-forks/feature/versions
Feature/versions
2020-03-05 14:20:31 +01:00
Alejandro Celaya
73e3f42614 Added ShlinkVersions test 2020-03-05 13:55:39 +01:00
Alejandro Celaya
e761f5e1bd Updated changelog 2020-03-05 13:45:24 +01:00
Alejandro Celaya
4a6dd66ecd Added scripts to pass version when building docker image 2020-03-05 13:37:07 +01:00
Alejandro Celaya
8e1c6908c6 Updated build script so that it replaces version placeholder when a version is provided 2020-03-05 13:27:57 +01:00
Alejandro Celaya
f59e569e22 Extracted logic to determine app version from function to generate dist file 2020-03-05 13:04:12 +01:00
Alejandro Celaya
be50b24504 Added mechanism to provide a version to shlink-web-client 2020-03-05 12:53:32 +01:00
Alejandro Celaya
c181831a37 Fixed tests 2020-03-05 11:58:35 +01:00
Alejandro Celaya
dbee62ac8c Moved shlink versions component to main container 2020-03-05 11:46:38 +01:00
Alejandro Celaya
1e949b3a22 Added shlink versions to side menu 2020-03-05 11:11:26 +01:00
Alejandro Celaya
b02dcf6c53 Refactored delete server components 2020-03-05 10:18:38 +01:00
Alejandro Celaya
ab7718e335 Removed duplicated code from AsideMenu by creating an AsideMenuItem helper component 2020-03-05 10:03:38 +01:00
Alejandro Celaya
451c77d47f Merge pull request #214 from acelaya-forks/feature/consistent-server-loading
Feature/consistent server loading
2020-03-05 09:32:59 +01:00
Alejandro Celaya
fa0d3d4047 Removed no longer needed async/await when building api client 2020-03-05 09:23:53 +01:00
Alejandro Celaya
397a183f65 Converted MenuLayout into a functional component with hooks 2020-03-05 09:08:50 +01:00
Alejandro Celaya
bc8905ee7f Ensured server is properly loaded before trying to render any children component 2020-03-05 08:59:07 +01:00
Alejandro Celaya
853032ac7f Displayed preloader when a server is being loaded 2020-03-05 08:41:55 +01:00
Alejandro Celaya
3b0e282a52 Merge pull request #211 from acelaya-forks/feature/jest-each
Feature/jest each
2020-02-17 18:32:52 +01:00
Alejandro Celaya
bb28cb3862 Updated changelog 2020-02-17 18:25:21 +01:00
Alejandro Celaya
d0f458bece Uninstalled jest-each and replaced by jest's native each 2020-02-17 18:21:52 +01:00
Alejandro Celaya
da54a72b3e Merge pull request #206 from acelaya-forks/feature/match-domain
Feature/match domain
2020-02-08 11:04:58 +01:00
Alejandro Celaya
86c155d8d1 Updated changelog 2020-02-08 10:47:44 +01:00
Alejandro Celaya
666d2d3065 Ensured domain is dispatched when modifying a short URL somehow 2020-02-08 10:46:11 +01:00
Alejandro Celaya
01e69fb6ca Merge pull request #204 from acelaya-forks/feature/multi-domain-fixes
Feature/multi domain fixes
2020-02-08 10:24:49 +01:00
Alejandro Celaya
30e5253acd Simplified instructions removing redundant vars 2020-02-08 10:07:34 +01:00
Alejandro Celaya
c67ce3918b Removed redundant function call 2020-02-08 10:03:24 +01:00
Alejandro Celaya
58077f2d86 Updated changelog 2020-02-08 09:59:13 +01:00
Alejandro Celaya
098c94bccf Ensured domain is passed when deleting a short URL on a specific domain 2020-02-08 09:57:18 +01:00
Alejandro Celaya
861a3c068f Ensured domain is passed when editing meta for a short URL on a specific domain 2020-02-08 09:52:30 +01:00
Alejandro Celaya
3b95e8ebc0 Ensured domain is passed when editing tags for a short URL on a specific domain 2020-02-08 09:48:35 +01:00
Alejandro Celaya
170e427530 Ensured domain is passed when loading detail for a short URL on a specific domain 2020-02-08 09:38:19 +01:00
Alejandro Celaya
707c9f4ce6 Created VisitStatsLink test 2020-02-08 09:22:17 +01:00
Alejandro Celaya
dc672bf0f0 Ensured domain is passed when loading visits for a short URL on a specific domain 2020-02-08 09:07:55 +01:00
Alejandro Celaya
c682737505 Standardized date-picker selected day color 2020-02-02 09:30:41 +01:00
Alejandro Celaya
46fa3d4345 Merge pull request #201 from acelaya-forks/feature/document-routing-fallback
Updated documentation
2020-01-31 20:51:17 +01:00
Alejandro Celaya
9b7bc4b495 Updated documentation 2020-01-31 20:37:50 +01:00
Alejandro Celaya
4385061499 Merge pull request #200 from acelaya-forks/feature/refactor-edit-tags-modal
Feature/refactor edit tags modal
2020-01-31 20:22:04 +01:00
Alejandro Celaya
e17498e68b Updated changelog 2020-01-31 20:13:18 +01:00
Alejandro Celaya
3e298f010b Simplified DeleteShortUrlModal component and shortUrlDeletion reducer 2020-01-31 20:12:22 +01:00
Alejandro Celaya
30117bd121 Simplified EditTagsModal component and shortUrlTags reducer 2020-01-31 20:06:28 +01:00
Alejandro Celaya
93f33b6218 Fixed some tests after not injecting a component 2020-01-31 20:04:03 +01:00
Alejandro Celaya
535d08a607 Merge pull request #197 from MartinH0/master
Add htaccess to redirect if not found to index
2020-01-31 16:21:30 +01:00
MartinH0
6ac3a49db2 Updated nginx.conf (optimization for future)
1. changed location from "~" (case sensitive!) to "~*" (case insensitive!) to also match uppercase static assets. (http://nginx.org/en/docs/http/ngx_http_core_module.html#location)
2. added regex "jpe?g" to match "jpg" and "jpeg" in one command.
2020-01-31 01:36:17 +01:00
MartinH0
c16f760d79 Update .htaccess
1. removed $ (dollar sign from line 14
2. changed line 8 from ".*" to "(.*)"
2020-01-31 01:29:41 +01:00
MartinH0
965c2b243f Update .htaccess
1. added more comments.
2. added NC Tag for making all the static assets case insensetive ("jpg" now matches "jpg" and "JPG" and so on)
3. transformed "jpe|jpeg" into "jpe?g" as its regex for the same, but shorter
4. changed line 8 from "+" to "*" to match everything, also zero times the wildcard
2020-01-31 01:27:13 +01:00
MartinH0
703addddb9 updated htaccess
deleted second json (just needed once)
2020-01-30 21:05:20 +01:00
MartinH0
ab6dff5c31 Updates htaccess
return 404 error if static assets does not exist
2020-01-30 20:51:23 +01:00
MartinH0
2ef330c62b Updated htaccess 2020-01-30 20:49:46 +01:00
MartinH0
72e71aff40 Updated htaccess to meet required functions.
If a file gets called it will be redirected to index.html

But not if it the requested File does contain a dot (and with this does have a file extension.

If you call:
links.domain.de/notexistingfile.jpg 
It will trigger 404

If you call:
links.domain.de/server/[CODE-CODE-CODE]/list-short-urls/1
It will redirect the call to index.html
2020-01-30 19:06:50 +01:00
MartinH0
cefd6ec752 Add htaccess to redirect if not found to index
If (file not found or directory not found)
then > redirect to index.html
2020-01-30 18:51:38 +01:00
MartinH0
aec3de18aa Deleted .htaccess at wrong directory
Sorry fucked it up, will correct it.
2020-01-30 18:51:32 +01:00
MartinH0
97620cb583 Add htaccess to redirect if not found to index
If file not fount or directory not found redirect to index.html
2020-01-30 18:36:02 +01:00
Alejandro Celaya
cf4e8190a4 Merge pull request #195 from acelaya-forks/feature/server-version-wrapper
Feature/server version wrapper
2020-01-28 19:55:24 +01:00
Alejandro Celaya
8af7436f13 Updated changelog 2020-01-28 19:47:41 +01:00
Alejandro Celaya
c53520ae56 Moved logic to dynamically render components based on server version to a separated component 2020-01-28 19:46:36 +01:00
Alejandro Celaya
3adcaef455 Merge pull request #194 from acelaya-forks/feature/fix-set-empty-max-visits
Fixed maxVisits being set to 0 when trying to reset it
2020-01-28 18:51:18 +01:00
Alejandro Celaya
43cd9722a9 Updated project to node 12.14.1 2020-01-28 18:40:33 +01:00
Alejandro Celaya
f3154e770e Fixed maxVisits being set to 0 when trying to reset it 2020-01-28 18:36:23 +01:00
Alejandro Celaya
44aca4aeda Merge pull request #192 from acelaya-forks/feature/version-constraints
Feature/version constraints
2020-01-19 21:37:56 +01:00
Alejandro Celaya
5762342d6c Ensured edit meta menu item is only displayed when shlink v1.18 or greater is run 2020-01-19 21:30:01 +01:00
Alejandro Celaya
2236ed467e Ensured date range filtering is only displayed if Shlink v1.21 ow higer is run 2020-01-19 21:25:45 +01:00
Alejandro Celaya
d244b830ac Updated date on license file 2020-01-19 21:20:32 +01:00
Alejandro Celaya
e89b68fe1e Merge pull request #190 from acelaya-forks/feature/edit-meta
Feature/edit meta
2020-01-19 21:15:25 +01:00
Alejandro Celaya
1f588c5b13 Updated changelog with v2.3.0 2020-01-19 21:00:31 +01:00
Alejandro Celaya
38cad143a0 Created EditMetaModal test 2020-01-19 20:59:01 +01:00
Alejandro Celaya
f52bcc5389 Ensured state is reset on edit meta modal after closing it 2020-01-19 20:37:12 +01:00
Alejandro Celaya
caa6f7bcd8 Created shortUrlMetaReducer test 2020-01-19 20:21:59 +01:00
Alejandro Celaya
207a8cef20 Updated tests from modified code 2020-01-19 13:20:46 +01:00
Alejandro Celaya
d44a4b260e Finished component to allow metadata to be edited on existing short URLs 2020-01-19 13:07:33 +01:00
Alejandro Celaya
80a8e0b55c Created component to edit short URLs meta 2020-01-17 21:07:59 +01:00
Alejandro Celaya
2d60f830f7 Improved icons on short URL menu 2020-01-15 20:25:58 +01:00
Alejandro Celaya
90751a09f7 Merge pull request #188 from acelaya-forks/feature/visits-amount
Feature/visits amount
2020-01-15 18:42:09 +01:00
Alejandro Celaya
301da4bb2a Recovered behavior to show amount of visits in selected date range on visits detail page 2020-01-15 18:31:28 +01:00
Alejandro Celaya
c90cd46095 Removed old ExternalLink component in favor of external one 2020-01-15 18:16:12 +01:00
Alejandro Celaya
7826000384 Merge pull request #187 from acelaya-forks/feature/date-filter
Feature/date filter
2020-01-14 20:59:01 +01:00
Alejandro Celaya
b48dcdd5e1 Fixed wrong files being picked for mutations 2020-01-14 20:48:01 +01:00
Alejandro Celaya
4f6326b139 Updated changelog 2020-01-14 20:21:14 +01:00
Alejandro Celaya
cff96eeccc Created DateRangeRow test 2020-01-14 20:20:27 +01:00
Alejandro Celaya
5eb4a3adec Fixed tests and typos 2020-01-14 20:12:30 +01:00
Alejandro Celaya
b60908a5e9 Added filtering by date to short URLs list 2020-01-14 19:59:25 +01:00
Alejandro Celaya
124441238b Moved style to the proper scope 2020-01-12 12:08:26 +01:00
Alejandro Celaya
4ec0287a74 Merge pull request #185 from Starbix/patch-2
Update nginx base image
2020-01-12 11:01:38 +01:00
Cédric Laubacher
05c67a5c99 Update nginx base image 2020-01-12 10:50:40 +01:00
Alejandro Celaya
f507a3628c Merge pull request #184 from acelaya-forks/feature/show-max-visits
Feature/show max visits
2020-01-11 20:23:02 +01:00
Alejandro Celaya
89e9d2b2d1 Fixed accidentally refactored string 2020-01-11 20:11:41 +01:00
Alejandro Celaya
595858ac4b Used visits count component in short URL visits view 2020-01-11 20:10:12 +01:00
Alejandro Celaya
3f2162fe62 Extracted visits count component to reuse it in other places 2020-01-11 19:58:04 +01:00
Alejandro Celaya
f2cb30409a Updated changelog 2020-01-11 19:41:41 +01:00
Alejandro Celaya
5c4fec5a2f Displayed amount of max visits on those URLs which have it 2020-01-11 19:40:16 +01:00
Alejandro Celaya
e96c119432 Merge pull request #183 from acelaya-forks/feature/support-shlink-2
Feature/support shlink 2
2020-01-11 14:25:25 +01:00
Alejandro Celaya
0920962d72 Used already unpacked property 2020-01-11 14:16:23 +01:00
Alejandro Celaya
aaeb0fff78 Updated changelog 2020-01-11 14:13:58 +01:00
Alejandro Celaya
de41f50945 Ensured preview menu item is hidden when consuming Shlink 2 2020-01-11 14:12:58 +01:00
Alejandro Celaya
0f51bf95e3 Updated ShlinkApiClient so that it retries API version when v2 is not supported 2020-01-11 13:55:37 +01:00
Alejandro Celaya
ba8cade6fc When handling API errors, use the type prop and fallback to error if not found 2020-01-11 12:24:45 +01:00
Alejandro Celaya
dbefae5a01 Merge pull request #173 from Starbix/patch-1
Update baseimages
2019-11-21 11:35:49 +01:00
Cédric Laubacher
727b219742 Update nginx image to latest version 2019-11-21 07:55:40 +01:00
Alejandro Celaya
fb25e44b58 Used strict version number for nginx base image 2019-11-21 07:52:50 +01:00
Cédric Laubacher
fe2d394831 Update baseimages
Nginx can be set to the latest patch version, as its API is really stable.
2019-11-16 10:24:34 +01:00
Alejandro Celaya
efd08ff1d6 Updated changelog 2019-11-10 13:04:15 +01:00
Alejandro Celaya
4b861a5376 Removed profanity 2019-11-10 08:47:13 +01:00
Alejandro Celaya
2076e7d5e8 Merge pull request #171 from MartinH0/master
Updated FavIcons
2019-11-10 08:44:59 +01:00
MartinH0
37f6f1f90c rename to favicon 2019-11-09 22:34:41 +01:00
MartinH0
81f76e0bd6 SVG FavIcon Fix
Added sizes="any" to the svg FavIcon
2019-11-09 22:32:13 +01:00
MartinH0
69b305cd8a Added SVG FavIcon
Added SVG FavIcon as File in Rootdirectory
2019-11-09 22:29:37 +01:00
MartinH0
45742a066e Added SVG FavIcon 2019-11-09 22:28:44 +01:00
MartinH0
86fb8b3f7c Added FavIcons
Added .gif and .png FavIcon
2019-11-09 19:38:50 +01:00
MartinH0
9c0fc8e1d2 Adjusted to FavIcons
Added MS Icons, Added all Apple Icons, Added all normal Icons and standard FavIcons
2019-11-09 19:37:31 +01:00
MartinH0
10d6302180 Added all FavIcons to Manifest
Added all missing FavIcons to the manifest
2019-11-09 19:22:55 +01:00
MartinH0
da7ed6992f FavIcon
Overwrite FavIcon with 128x128 new optimized FavIcon
2019-11-09 19:17:45 +01:00
MartinH0
32c9375ac8 FavIcons edits
Added new FavIcons and overwrite old ones
2019-11-09 19:16:12 +01:00
MartinH0
7ed1334a51 Updated FavIcons
Added all Versions of all available FavIcons.
Also Added normal FavIcons additionally to the apple ones.
Also added the "msapplication-TileImage" meta for Microsoft.
2019-11-07 21:47:57 +01:00
Alejandro Celaya
d9097896f6 Added github funding 2019-10-22 19:33:59 +02:00
Alejandro Celaya
359b16e700 Merge pull request #168 from acelaya-forks/feature/default-servers-issue
Feature/default servers issue
2019-10-21 19:52:31 +02:00
Alejandro Celaya
0237af771d Fixed outdated comment 2019-10-21 19:45:35 +02:00
Alejandro Celaya
86cce5b205 Updated changelog 2019-10-21 19:39:59 +02:00
Alejandro Celaya
fc7a2e0c6d Ensured response from servers.json has been parsed to a json array 2019-10-21 19:38:32 +02:00
Alejandro Celaya
f74d135922 Ensured default servers is validated as JSON and ignored otherwise 2019-10-21 19:26:09 +02:00
Alejandro Celaya
66124370a6 Added json extension to the list of known static files that have to fall back to 404 on nginx 2019-10-21 18:49:47 +02:00
Alejandro Celaya
e9fc2bb73a Merge pull request #166 from acelaya-forks/feature/fix-create-short-url
Ensured server version is properly parsed to avoid errors due to inva…
2019-10-18 17:48:02 +02:00
Alejandro Celaya
12f6b94ece Ensured server version is properly parsed to avoid errors due to invalid semver 2019-10-18 17:39:38 +02:00
Alejandro Celaya
d9a8243d36 Merge pull request #163 from acelaya-forks/feature/update-deps
Feature/update deps
2019-10-05 20:09:22 +02:00
Alejandro Celaya
232c54885e Updated node version in which builds are run 2019-10-05 19:58:27 +02:00
Alejandro Celaya
42c43f6c78 Added v2.2 to changelog 2019-10-05 19:54:10 +02:00
Alejandro Celaya
9d2494834c Fixed timing issue when navigating to another server 2019-10-05 19:51:50 +02:00
Alejandro Celaya
a7613435ea Fixed test throwing unhandled promise 2019-10-05 19:31:47 +02:00
Alejandro Celaya
c9df044e1a Updated docker image versions 2019-10-05 19:26:06 +02:00
Alejandro Celaya
5a37787042 Fixed warnings in tests 2019-10-05 19:13:57 +02:00
Alejandro Celaya
923cc3ba01 Updated dev dependencies 2019-10-05 19:08:50 +02:00
Alejandro Celaya
8fcf72f564 Updated production dependencies to latest versions 2019-10-05 18:50:49 +02:00
Alejandro Celaya
a7f7666ccd Merge pull request #162 from acelaya-forks/feature/domain
Feature/domain
2019-10-05 11:15:09 +02:00
Alejandro Celaya
c181948afe Updated changelog 2019-10-05 11:05:03 +02:00
Alejandro Celaya
ce9ecd7b93 Defined custom function to compare versions which defines the operator in the middle 2019-10-05 11:03:17 +02:00
Alejandro Celaya
354d19af1b Disabled domain component for Shlink versions not supporting it 2019-10-05 10:54:58 +02:00
Alejandro Celaya
6d996baf5d Added tests for new logics 2019-10-05 10:40:32 +02:00
Alejandro Celaya
4120d09220 Loaded version of selected server and created component to filter content based on that version 2019-10-05 10:20:33 +02:00
Alejandro Celaya
67a23bfe33 Added domain input to create short url form 2019-10-05 09:02:02 +02:00
Alejandro Celaya
08b710930d Merge pull request #161 from acelaya-forks/feature/further-issue-template-improvements
Solved inconsistencies in issue templates due to copy-pasting from ot…
2019-09-29 09:46:57 +02:00
Alejandro Celaya
7ec3b332ed Solved inconsistencies in issue templates due to copy-pasting from other project 2019-09-29 09:46:19 +02:00
Alejandro Celaya
722eb060f0 Merge pull request #160 from acelaya-forks/feature/improved-issue-templates
Added improved issue templates and funding config
2019-09-29 09:42:45 +02:00
Alejandro Celaya
ce740aed68 Added improved issue templates and funding config 2019-09-29 09:36:57 +02:00
Alejandro Celaya
09f582daa1 Merge pull request #159 from acelaya-forks/feature/fix-docker-image-reload
Added nginx congif which ensures client-side paths are served as the …
2019-09-22 12:02:18 +02:00
Alejandro Celaya
1b5f7b0d76 Added nginx congif which ensures client-side paths are served as the index.html 2019-09-22 11:55:21 +02:00
Alejandro Celaya
2c93e9a587 Merge pull request #158 from acelaya-forks/feature/improved-pagination
Feature/improved pagination
2019-09-22 11:44:54 +02:00
Alejandro Celaya
ab0976981b Fixed style files being excluded when finding what files to mutate 2019-09-22 11:35:52 +02:00
Alejandro Celaya
959ce42137 Updated changelog 2019-09-22 11:16:16 +02:00
Alejandro Celaya
1c25db9179 Created SimplePaginator test 2019-09-22 11:14:08 +02:00
Alejandro Celaya
810ddd7717 Added foldable pagination to SimplePaginator 2019-09-22 10:41:31 +02:00
Alejandro Celaya
7bbff114a4 Extracted paginator used in SortableBarGraph to its own component 2019-09-21 18:29:58 +02:00
Alejandro Celaya
99475fc311 Merge pull request #154 from Haocen/151
Fix an inaccurate variable name in test
2019-09-15 15:26:17 +02:00
Haocen Xu
df121eb294 Fix an inaccurate variable name in test 2019-09-15 09:14:00 -04:00
Alejandro Celaya
138194a149 Merge pull request #153 from Haocen/151
When no order is specified, the order by indicator(triangle) in column header should be Cleared
2019-09-15 09:30:11 +02:00
Haocen Xu
ab99213d8c When no order is specified, the order by indicator(triangle) in column header should be Cleared 2019-09-14 18:13:15 -04:00
Alejandro Celaya
2fe923678e Installed react-external-links 2019-08-29 17:47:18 +02:00
Alejandro Celaya
34f194c714 Merge pull request #143 from acelaya-forks/feature/docker-versions-bump
Feature/docker versions bump
2019-08-24 16:42:22 +02:00
Alejandro Celaya
2bef398d4c Added repository and license fields to package.json 2019-08-24 16:38:44 +02:00
Alejandro Celaya
404b5c45dd Updated changelog 2019-08-24 16:33:23 +02:00
Alejandro Celaya
f607ade508 Bumbed version of docker images 2019-08-24 16:31:54 +02:00
Alejandro Celaya
158ed84ec5 Updated changelog 2019-05-19 20:31:57 +02:00
Alejandro Celaya
7c22713d7d Merge pull request #139 from acelaya/feature/coverage-80
Created tags list reducer test
2019-05-19 20:30:13 +02:00
Alejandro Celaya
fb94077260 Created shortUrlTags reducer test 2019-05-19 13:22:16 +02:00
Alejandro Celaya
d3491869bd Created tags list reducer test 2019-05-19 12:54:19 +02:00
Alejandro Celaya
5cefadbf37 Added missing link in changelog to docs on new feature 2019-05-19 11:42:24 +02:00
Alejandro Celaya
95462b0c1d Merge pull request #137 from acelaya/feature/pre-configure-servers
Feature/pre configure servers
2019-04-28 18:01:46 +02:00
Alejandro Celaya
258330f985 Mentioned that pre-configured servers won't work on versions previous to 2.1.0 2019-04-28 17:53:35 +02:00
Alejandro Celaya
a09b661b51 Updated changelog 2019-04-28 17:42:20 +02:00
Alejandro Celaya
a1a0b935c7 Improved documentation mentioning how to pre-configure servers 2019-04-28 17:41:01 +02:00
Alejandro Celaya
4c11d9c6d5 Catched error when loading servers from a servers.json file 2019-04-28 13:07:55 +02:00
Alejandro Celaya
78c34a342d Added tests for new use cases 2019-04-28 12:40:50 +02:00
Alejandro Celaya
20820c47d4 Updated list servers action so that it tries to fetch servers from the servers.json file when no local servers are found 2019-04-28 12:07:09 +02:00
Alejandro Celaya
502c8a7e02 Echoing travis commit range 2019-04-22 19:13:02 +02:00
Alejandro Celaya
ce8a198acd Merge pull request #136 from acelaya/feature/stryker
Feature/stryker
2019-04-22 10:11:32 +02:00
Alejandro Celaya
32f171d861 Updated travis to run mutations on changed files only 2019-04-22 10:04:41 +02:00
Alejandro Celaya
b83c0e0aba Improved stryker config 2019-04-21 23:18:35 +02:00
Alejandro Celaya
831c0444d6 Added stryker to the project 2019-04-21 23:03:53 +02:00
Alejandro Celaya
e5ef2eb5c6 Merge pull request #135 from acelaya/feature/shlink-client-improvements
Removed duplicated code when building ShlinkApiClient
2019-04-21 11:37:14 +02:00
Alejandro Celaya
7b80d78dc5 Removed duplicated code when building ShlinkApiClient 2019-04-21 11:31:40 +02:00
Alejandro Celaya
48f7103205 Merge pull request #134 from acelaya/feature/jest-mocks
Feature/jest mocks
2019-04-19 13:02:28 +02:00
Alejandro Celaya
bc8de096be Updated changelog 2019-04-19 12:55:41 +02:00
Alejandro Celaya
ba3189fd46 Removed no longer needed constants 2019-04-19 12:54:56 +02:00
Alejandro Celaya
33d67cbe3d Simplified code making it easier to read 2019-04-19 12:52:55 +02:00
Alejandro Celaya
28ca54547e Removed remaining usages of sinon 2019-04-19 12:41:59 +02:00
Alejandro Celaya
f8de069567 First replacements of sinon mocks with jest mocks 2019-04-19 10:29:49 +02:00
Alejandro Celaya
2cd6e52e9c Merge pull request #133 from acelaya/feature/remove-yarn
Replaced yarn by npm
2019-04-14 22:06:59 +02:00
Alejandro Celaya
372d3f17cc Replaced yarn by npm 2019-04-14 21:58:10 +02:00
Alejandro Celaya
92d5b2eb3e Merge pull request #132 from acelaya/feature/issue-template
Created issue template with some reminders
2019-04-11 22:11:12 +02:00
Alejandro Celaya
6be55e30d9 Dockerignored .gihub folder 2019-04-11 22:03:53 +02:00
Alejandro Celaya
fd517ccbe2 Created issue template with some reminders 2019-04-11 22:01:11 +02:00
Alejandro Celaya
c2a34b4079 Merge pull request #127 from acelaya/feature/check-existing
Feature/check existing
2019-03-17 18:41:22 +01:00
Alejandro Celaya
ce0f036bef Created custom react hook that can be used to handle toggles 2019-03-17 18:35:47 +01:00
Alejandro Celaya
977e143b4e Fixed coding styles 2019-03-17 18:24:09 +01:00
Alejandro Celaya
d847ccf0f4 Updated changelog 2019-03-17 18:17:29 +01:00
Alejandro Celaya
7eeed76539 Created UseExistingIfFoundInfoIcon test 2019-03-17 18:15:44 +01:00
Alejandro Celaya
2e452993ff Created Checkbox test 2019-03-17 18:09:10 +01:00
Alejandro Celaya
f4dbd03c7e Added checkbox to control the findIfExists shlink flag 2019-03-17 17:48:05 +01:00
Alejandro Celaya
312c6cd550 Merge pull request #126 from acelaya/feature/redux-actions
Feature/redux actions
2019-03-17 10:31:13 +01:00
Alejandro Celaya
8d9e8565f0 Updated changelog 2019-03-17 10:23:17 +01:00
Alejandro Celaya
d1c10e4895 Removed test cases for the old default on reducers switch statements 2019-03-17 10:17:44 +01:00
Alejandro Celaya
232c059e4f Replaced usages of defaultState by initialState 2019-03-17 10:11:20 +01:00
Alejandro Celaya
5bb9d15e27 Refactored tagEdit reducer to take advantage of redux-actions 2019-03-17 10:07:28 +01:00
Alejandro Celaya
879034c9c6 Refactored tagDelete reducer to take advantage of redux-actions 2019-03-17 10:02:44 +01:00
Alejandro Celaya
740aacbbf1 Refactored tagsList reducer to take advantage of redux-actions 2019-03-17 09:59:26 +01:00
Alejandro Celaya
fcfab79bed Refactored shortUrlDetail reducer to take advantage of redux-actions 2019-03-17 09:38:37 +01:00
Alejandro Celaya
468e34aa3d Refactored shortUrlVisits reducer to take advantage of redux-actions 2019-03-17 09:36:07 +01:00
Alejandro Celaya
7ff7318089 Refactored shortUrlTags reducer to take advantage of redux-actions 2019-03-17 09:32:53 +01:00
Alejandro Celaya
4654bff737 Refactored shortUrlDeletion reducer to takle advantage of redux-actions 2019-03-17 09:27:01 +01:00
Alejandro Celaya
3075ccb4b9 Refactored shortUrlCreation reducer to takle advantage of redux-actions 2019-03-17 09:20:02 +01:00
Alejandro Celaya
4894ab9035 Refactored shortUrlsListParams reducer to takle advantage of redux-actions 2019-03-17 09:15:58 +01:00
Alejandro Celaya
4a09d99322 Refactored shortUrlsList to take advantage of redux-actions 2019-03-17 09:11:37 +01:00
Alejandro Celaya
51b5f6264d Refactored server reducer, removing duplicated code and taking advantage of redux-actions 2019-03-17 09:06:10 +01:00
Alejandro Celaya
724c804971 Installed redux-actions dependency and used it for selectedServer reducer 2019-03-17 08:49:24 +01:00
229 changed files with 25310 additions and 13060 deletions

View File

@@ -1,5 +1,6 @@
./.github
./build
./coverage
./dist
./node_modules
./test
./shlink-web-client.gif

View File

@@ -28,6 +28,8 @@
"no-warning-comments": "off",
"no-magic-numbers": "off",
"no-undefined": "off",
"no-inline-comments": "off",
"lines-around-comment": "off",
"indent": ["error", 2, {
"SwitchCase": 1
}

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: ['acelaya']
custom: ['https://acel.me/donate']

6
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,6 @@
<!--
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
-->

35
.github/ISSUE_TEMPLATE/Bug.md vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: Bug report
about: Something on shlink is broken or not working as documented?
labels: bug
---
<!--
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
With that said, please fill in the information requested next. More information might be requested once the issue is open.
-->
#### Shlink web client version
* Version: x.y.z
* How do you use shlink-web-client: app.shlink.io|Docker image|self-hosted
#### Summary
<!-- Provide a summary describing the problem you are experiencing. -->
#### Current behavior
<!-- How is it actually behaving (and it shouldn't)? -->
#### Expected behavior
<!-- How did you expected to behave? -->
#### How to reproduce
<!-- Provide steps to reproduce the bug. -->

View File

@@ -0,0 +1,18 @@
---
name: Feature request
about: Do you find shlink is missing some important feature that would make it more useful?
labels: feature
---
<!--
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
With that said, please fill in the information requested next. More information might be requested once the issue is open.
-->
#### Summary
<!-- Describe the new feature you would like to request. -->

View File

@@ -0,0 +1,23 @@
---
name: Question - Support
about: Do you have a problem setting up or using shlink?
labels: question
---
<!--
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
With that said, please fill in the information requested next. More information might be requested once the issue is open.
-->
#### Shlink web client version
* Version: x.y.z
* How do you use shlink-web-client: app.shlink.io|Docker image|self-hosted
#### Summary
<!-- Describe the issue you are facing here. -->

12
.gitignore vendored
View File

@@ -3,18 +3,14 @@
# testing
/coverage
/.stryker-tmp
/reports
# production
/build
# misc
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
docker-compose.override.yml
home
public/servers.json*

View File

@@ -1,5 +1,6 @@
build:
environment:
node: v10.4.1
node: v12.14.1
tools:
external_code_coverage: true
external_code_coverage:
timeout: 1200

View File

@@ -1,10 +1,9 @@
language: node_js
node_js:
- "10.15.3"
- "12.14.1"
cache:
yarn: true
directories:
- node_modules
@@ -12,25 +11,35 @@ services:
- docker
install:
- yarn install
- npm ci
before_script:
- echo "Building commit range ${TRAVIS_COMMIT_RANGE}"
- export MUTATION_FILES=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/master} --name-only | grep -E 'src\/(.*).(js|ts|jsx|tsx)$' | paste -sd ",")
script:
- yarn lint
- yarn test:ci
- if [[ -z $TRAVIS_TAG ]]; then docker build -t shlink-web-client:test . ; fi # Test docker image build only when no tag is present
- npm run lint
- npm run test:ci
- if [[ $TRAVIS_PULL_REQUEST != 'false' ]]; then docker build -t shlink-web-client:test . ; fi
- if [[ $TRAVIS_PULL_REQUEST != 'false' ]]; then npm run mutate:ci ; fi
after_success:
- yarn ocular coverage/clover.xml
- node_modules/.bin/ocular coverage/clover.xml
# Before deploying, build dist file for current travis tag
before_deploy:
- yarn build ${TRAVIS_TAG#?}
- if [[ ! -z $TRAVIS_TAG ]]; then npm run build ${TRAVIS_TAG#?} ; fi
deploy:
provider: releases
api_key:
secure: jBvPwC7EAbViaNR83rwMSt5XQDK0Iu9rgvEMa7GoyShbHcvUCCPd73Tu9quNpKi6NKsDY3INHgtch3vgonjGNGDGJ+yDyIBzXcvsAX2x3UcHpRbgY12uiINVmQxBI1+OVQB016Nm+cKC/i5Z36K4EmDbYfo+MrKndngM6AjcQFTwI8EwniIMaQgg4gNes//K8NhP5u0c3gwG+Q6jEGnq6uH3kcRgh6/epIZYpQyxjqWqKwF77sgcYj+X2Nf6XxtB5neuCi301UKLoLx8G0skh/Lm6KAIO4s9iIhIFa3UpoF21Ka0TxLpd2JxalLryCnFGlWWE6lxC9Htmc0TeRowJQlGdJXCskJ37xT9MljKY0fwNMu06VS/FUgykuCv+jP3zQu51pKu7Ew7+WeNPjautoOTu54VkdGyHcf2ThBNEyJQuiEwAQe4u7yAxY6R5ovEdvHBSIg4w1E5/Mxy5SMTCUlIAv6H7QQ1X9Z/zJm9HH5KeKz5tsHvQ/RIdSpgHXq/tC8o4Yup/LCFucXfrgvy/8pJoO1UpOlmvm62974NFfo0EG5YWwv6brUqz3QXpMjb8sWqgjltYMYJX3J7WZ34rIc+zt4NAmfhqgczaOC4pUGCiJ8jX3rMWIaQRn1AJ+5V337jL9fNDpTHny4phQjHrMJ1e0HZuNp0Xb5Q8wgqDPM=
file: "./dist/shlink-web-client_${TRAVIS_TAG#?}_dist.zip"
skip_cleanup: true
on:
tags: true
- provider: script
script: bash ./scripts/docker/build
on:
all_branches: true
condition: $TRAVIS_PULL_REQUEST == 'false'
- provider: releases
api_key:
secure: jBvPwC7EAbViaNR83rwMSt5XQDK0Iu9rgvEMa7GoyShbHcvUCCPd73Tu9quNpKi6NKsDY3INHgtch3vgonjGNGDGJ+yDyIBzXcvsAX2x3UcHpRbgY12uiINVmQxBI1+OVQB016Nm+cKC/i5Z36K4EmDbYfo+MrKndngM6AjcQFTwI8EwniIMaQgg4gNes//K8NhP5u0c3gwG+Q6jEGnq6uH3kcRgh6/epIZYpQyxjqWqKwF77sgcYj+X2Nf6XxtB5neuCi301UKLoLx8G0skh/Lm6KAIO4s9iIhIFa3UpoF21Ka0TxLpd2JxalLryCnFGlWWE6lxC9Htmc0TeRowJQlGdJXCskJ37xT9MljKY0fwNMu06VS/FUgykuCv+jP3zQu51pKu7Ew7+WeNPjautoOTu54VkdGyHcf2ThBNEyJQuiEwAQe4u7yAxY6R5ovEdvHBSIg4w1E5/Mxy5SMTCUlIAv6H7QQ1X9Z/zJm9HH5KeKz5tsHvQ/RIdSpgHXq/tC8o4Yup/LCFucXfrgvy/8pJoO1UpOlmvm62974NFfo0EG5YWwv6brUqz3QXpMjb8sWqgjltYMYJX3J7WZ34rIc+zt4NAmfhqgczaOC4pUGCiJ8jX3rMWIaQRn1AJ+5V337jL9fNDpTHny4phQjHrMJ1e0HZuNp0Xb5Q8wgqDPM=
file: "./dist/shlink-web-client_${TRAVIS_TAG#?}_dist.zip"
skip_cleanup: true
on:
tags: true

View File

@@ -4,6 +4,218 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
## 2.4.0 - 2020-04-10
#### Added
* [#199](https://github.com/shlinkio/shlink-web-client/issues/199) Added table to visits page which displays the information in a paginated, sortable and filterable list.
It also supports selecting multiple visits in the table which makes the corresponding data to be highlighted in the visits charts.
* [#241](https://github.com/shlinkio/shlink-web-client/issues/241) Added support to select charts bars in order to highlight related stats in other charts.
It also selects the visits in the new table, and you can even combine a selection in the chart and in the table.
* [#213](https://github.com/shlinkio/shlink-web-client/issues/213) The versions of both shlink-web-client and currently consumed Shlink server are now displayed in the footer.
* [#221](https://github.com/shlinkio/shlink-web-client/issues/221) Improved how servers are handled, displaying meaningful errors when a not-found or a not-reachable server is tried to be loaded.
* [#226](https://github.com/shlinkio/shlink-web-client/issues/226) Created servers can now be edited.
* [#234](https://github.com/shlinkio/shlink-web-client/issues/234) Allowed short code length to be edited on any new short URL when using Shlink 2.1 or higher.
* [#235](https://github.com/shlinkio/shlink-web-client/issues/235) Allowed editing the long URL for any existing short URL when suing Shlink 2.1 or higher.
#### Changed
* [#205](https://github.com/shlinkio/shlink-web-client/issues/205) Replaced `jest-each` package by jet's native `test.each` function.
* [#209](https://github.com/shlinkio/shlink-web-client/issues/209) Replaced `Unknown` by `Direct` for visits from undetermined referrers.
* [#212](https://github.com/shlinkio/shlink-web-client/issues/212) Moved copy-to-clipboard next to short URL.
* [#208](https://github.com/shlinkio/shlink-web-client/issues/208) Short URLs list paginator is now progressive.
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#243](https://github.com/shlinkio/shlink-web-client/issues/243) Fixed loading state and resetting on short URL creation form.
* [#239](https://github.com/shlinkio/shlink-web-client/issues/239) Fixed how user agents are parsed, reducing false results.
## 2.3.1 - 2020-02-08
#### Added
* *Nothing*
#### Changed
* [#191](https://github.com/shlinkio/shlink-web-client/issues/191) Created `ForServerVersion` helper component which dynamically renders children if current server conditions are met.
* [#189](https://github.com/shlinkio/shlink-web-client/issues/189) Simplified short url tags and short url deletion components and reducers, by removing redundant actions.
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#193](https://github.com/shlinkio/shlink-web-client/issues/193) Fixed `maxVisits` being set to 0 when trying to reset it from having a value to `null`.
* [#196](https://github.com/shlinkio/shlink-web-client/issues/196) Included apache `.htaccess` file which takes care of falling back to index.html when reloading the page on a client-side handled route.
* [#179](https://github.com/shlinkio/shlink-web-client/issues/179) Ensured domain is provided to Shlink server when editing, deleting or fetching short URLs which do not belong to default domain.
* [#202](https://github.com/shlinkio/shlink-web-client/issues/202) Fixed domain not passed when dispatching actions that affect a single short URL (edit tags, edit meta and delete), which cased the list not to be properly updated.
## 2.3.0 - 2020-01-19
#### Added
* [#174](https://github.com/shlinkio/shlink-web-client/issues/174) Added complete support for Shlink v2.x together with currently supported Shlink versions.
* [#164](https://github.com/shlinkio/shlink-web-client/issues/164) Added max visits control on those URLs which have `maxVisits`.
* [#178](https://github.com/shlinkio/shlink-web-client/issues/178) Short URLs list can now be filtered by date range.
* [#46](https://github.com/shlinkio/shlink-web-client/issues/46) Allowed short URL's metadata to be edited (`maxVisits`, `validSince` and `validUntil`).
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#170](https://github.com/shlinkio/shlink-web-client/issues/170) Fixed apple icon referencing to incorrect file names.
## 2.2.2 - 2019-10-21
#### Added
* *Nothing*
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#167](https://github.com/shlinkio/shlink-web-client/issues/167) Fixed `/servers.json` path not being ignored when returning something other than an array.
## 2.2.1 - 2019-10-18
#### Added
* *Nothing*
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#165](https://github.com/shlinkio/shlink-web-client/issues/165) Fixed error thrown when opening "create" page while using a Shlink version which does not return a valid SemVer version (like `latest` docker image, or any development instance).
## 2.2.0 - 2019-10-05
#### Added
* [#144](https://github.com/shlinkio/shlink-web-client/issues/144) Added domain input to create domain page.
#### Changed
* [#140](https://github.com/shlinkio/shlink-web-client/issues/140) Updated project dependencies.
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* *Nothing*
## 2.1.1 - 2019-09-22
#### Added
* *Nothing*
#### Changed
* [#142](https://github.com/shlinkio/shlink-web-client/issues/142) Updated to newer versions of base docker images for dev and production.
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#151](https://github.com/shlinkio/shlink-web-client/issues/151) Fixed "order by" indicator (caret) still indicate ASC on column header when no order is specified.
* [#157](https://github.com/shlinkio/shlink-web-client/issues/157) Fixed pagination control on graphs expanding too much when lots of pages need to be rendered.
* [#155](https://github.com/shlinkio/shlink-web-client/issues/155) Fixed client-side paths resolve to 404 when served from nginx in docker image instead of falling back to `index.html`.
## 2.1.0 - 2019-05-19
#### Added
* [#101](https://github.com/shlinkio/shlink-web-client/issues/101) Added checkbox to short URL creation form that allows to determine the value of the `findIfExists` flag introduced in Shlink v1.16.0.
* [#105](https://github.com/shlinkio/shlink-web-client/issues/105) Added support to pre-configure servers. See [how to pre-configure servers](README.md#pre-configuring-servers) to get more details on how to do it.
#### Changed
* [#125](https://github.com/shlinkio/shlink-web-client/issues/125) Refactored reducers to replace `switch` statements by `handleActions` from [redux-actions](https://github.com/redux-utilities/redux-actions).
* [#116](https://github.com/shlinkio/shlink-web-client/issues/116) Removed sinon in favor of jest mocks.
* [#72](https://github.com/shlinkio/shlink-web-client/issues/72) Increased code coverage up to 80%.
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* *Nothing*
## 2.0.3 - 2019-03-16
#### Added

View File

@@ -1,8 +1,17 @@
FROM node:10.15.3-alpine as node
FROM node:12.14.1-alpine as node
COPY . /shlink-web-client
RUN cd /shlink-web-client && yarn install && yarn build
ARG VERSION="latest"
ENV VERSION ${VERSION}
RUN cd /shlink-web-client && \
UNCOMPRESSED="shlink-web-client_${VERSION}_dist" && \
DIST_FILE="./dist/${UNCOMPRESSED}.zip" && \
# If a dist file already exists, just unzip it
if [[ -f ${DIST_FILE} ]]; then unzip ${DIST_FILE} && mv ./${UNCOMPRESSED} ./build ; fi && \
# If no dist file exsts, build from scratch
if [[ ! -f ${DIST_FILE} ]]; then npm install && npm run build -- ${VERSION} --no-dist ; fi
FROM nginx:1.15.9-alpine
FROM nginx:1.17.7-alpine
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
RUN rm -r /usr/share/nginx/html
RUN rm -r /usr/share/nginx/html && rm /etc/nginx/conf.d/default.conf
COPY config/docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=node /shlink-web-client/build /usr/share/nginx/html

View File

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

View File

@@ -1,51 +1,92 @@
# shlink-web-client
[![Build Status](https://img.shields.io/travis/shlinkio/shlink-web-client.svg?style=flat-square)](https://travis-ci.org/shlinkio/shlink-web-client)
[![Docker build status](https://img.shields.io/docker/cloud/build/shlinkio/shlink-web-client.svg?style=flat-square)](https://hub.docker.com/r/shlinkio/shlink-web-client/)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/shlinkio/shlink-web-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink-web-client/?branch=master)
[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/shlinkio/shlink-web-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink-web-client/?branch=master)
[![GitHub release](https://img.shields.io/github/release/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/releases/latest)
[![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink-web-client.svg?style=flat-square)](https://hub.docker.com/r/shlinkio/shlink-web-client/)
[![GitHub license](https://img.shields.io/github/license/shlinkio/shlink-web-client.svg?style=flat-square)](https://github.com/shlinkio/shlink-web-client/blob/master/LICENSE)
[![Paypal Donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=cccccc)](https://acel.me/donate)
[![Paypal Donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=cccccc)](https://slnk.to/donate)
A ReactJS-based progressive web application for [Shlink](https://shlink.io).
![shlink-web-client](shlink-web-client.gif)
## Installation
There are three ways in which you can use this application.
* The easiest way to use shlink-web-client is by just going to https://app.shlink.io.
### From app.shlink.io
The application runs 100% in the browser, so you can use that instance and access any shlink instance from it.
The easiest way to use shlink-web-client is by just going to <https://app.shlink.io>.
* Self hosting the application yourself.
The application runs 100% in the browser, so you can safely access any shlink instance from there.
Get the [latest release](https://github.com/shlinkio/shlink-web-client/releases/latest) and download the distributable zip file attached to it (`shlink-web-client_X.X.X_dist.zip`).
### Docker image
The package contains static files only, so just put it in a folder and serve it with the web server of your choice.
If you want to deploy shlink-web-client in a container-based cluster (kubernetes, docker swarm, etc), just pick the [shlinkio/shlink-web-client](https://hub.docker.com/r/shlinkio/shlink-web-client/) image and do it.
Provided dist files are configured to be served from the root of your domain. If you need to serve shlink-web-client from a subpath, you will have to build it yourself following [these simple steps](#serve-shlink-in-subpath).
It's a lightweight [nginx:alpine](https://hub.docker.com/r/library/nginx/) image serving the static app on port 80.
* Use the official [docker image](https://hub.docker.com/r/shlinkio/shlink-web-client/)
### Self-hosted
If you want to deploy shlink-web-client in a container-based cluster (kubernetes, docker swarm, etc), just pick the image and do it.
If you want to self-host it yourself, get the [latest release](https://github.com/shlinkio/shlink-web-client/releases/latest) and download the distributable zip file attached to it (`shlink-web-client_X.X.X_dist.zip`).
It's a lightweight [nginx:alpine](https://hub.docker.com/r/library/nginx/) image serving the assets on port 80.
The package contains static files only, so just put it in a folder and serve it with the web server of your choice.
**Considerations**:
* Provided dist files are configured to be served from the root of your domain. If you need to serve shlink-web-client from a subpath, you will have to build it yourself following [these steps](#serve-shlink-in-subpath).
* The app has a client-side router that handles dynamic paths. Because of that, you need to configure your web server to fall-back to the `index.html` file when requested files do not exist.
* If you use Apache, you are covered, since the project includes an `.htaccess` file which already does this.
* If you use nginx, you can [see how it's done](config/docker/nginx.conf) for the docker image and do the same.
## Pre-configuring servers
The first time you access shlink-web-client from a browser, you will have to configure the list of shlink servers you want to manage, and they will be saved in the local storage.
Those servers can be exported and imported in other browsers, but if for some reason you need some servers to be there from the beginning, starting with shlink-web-client 2.1.0, you can provide a `servers.json` file in the project root folder (the same containing the `index.html`, `favicon.ico`, etc) with a structure like this:
```json
[
{
"name": "Main server",
"url": "https://doma.in",
"apiKey": "09c972b7-506b-49f1-a19a-d729e22e599c"
},
{
"name": "Local",
"url": "http://localhost:8080",
"apiKey": "580d0b42-4dea-419a-96bf-6c876b901451"
}
]
```
> The list can contain as many servers as you need.
If you are using the shlink-web-client docker image, you can mount the `servers.json` file in a volume inside `/usr/share/nginx/html`, which is the app's document root inside the container.
docker run --name shlink-web-client -p 8000:80 -v ${PWD}/servers.json:/usr/share/nginx/html/servers.json shlinkio/shlink-web-client
## Serve project in subpath
Official distributable files have been build so that they are served from the root of a domain.
Official distributable files have been built so that they are served from the root of a domain.
If you need to host shlink-web-client yourself and serve it from a subpath, follow these steps:
* Download [node](https://nodejs.org/en/download/package-manager/) 10.4 or later (if you don't have it yet).
* Download [yarn](https://yarnpkg.com/en/docs/install) package manager.
* Download shlink-web-client source files for the version you want to build.
* Download shlink-web-client source code for the version you want to build.
* For example, if you want to build `v1.0.1`, use this link https://github.com/shlinkio/shlink-web-client/archive/v1.0.1.zip
* Replace the `v1.0.1` part in the link with the one of the version you want to build.
* Decompress the file and `cd` into the resulting folder.
* Install project dependencies by running `yarn install`.
* Open the `package.json` file in the root of the project, locate the `homepage` property and replace the value (which should be an empty string) by the path from which you want to serve shlink-web-client.
* For example: `"homepage": "/my-projects/shlink-web-client",`.
* Build the distributable contents by running `yarn build`.
* Once the command finishes, you will have a `build` folder with all the static assets you need to run shlink-web-client. Just place them wherever you want them to be served from.
* Build the project:
* For classic hosting:
* Download [node](https://nodejs.org/en/download/package-manager/) 10.15 or later.
* Install project dependencies by running `npm install`.
* Build the project by running `npm run build`.
* Once the command finishes, you will have a `build` folder with all the static assets you need to run shlink-web-client. Just place them wherever you want them to be served from.
* For docker image:
* Download [docker](https://docs.docker.com/install/).
* Build the docker image by running `docker build . -t shlink-web-client`.
* Once the command finishes, you will have an image with the name `shlink-web-client`.

17
config/docker/nginx.conf Normal file
View File

@@ -0,0 +1,17 @@
server {
listen 80 default_server;
charset utf-8;
root /usr/share/nginx/html;
index index.html;
# When requesting static paths with extension, try them, and return a 404 if not found
location ~* .+\.(css|js|html|png|jpe?g|gif|bmp|ico|json|csv|otf|eot|svg|svgz|ttf|woff|woff2|ijmap|pdf|tif|map) {
try_files $uri $uri/ =404;
}
# When requesting a path without extension, try it, and return the index if not found
# This allows HTML5 history paths to be handled by the client application
location / {
try_files $uri $uri/ /index.html$is_args$args;
}
}

View File

@@ -6,3 +6,4 @@ services:
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
- ./home:/home/alejandro

View File

@@ -3,8 +3,8 @@ version: '3'
services:
shlink_web_client_node:
container_name: shlink_web_client_node
image: node:10.15.0-alpine
command: /bin/sh -c "cd /home/shlink/www && yarn install && yarn start"
image: node:12.14.1-alpine
command: /bin/sh -c "cd /home/shlink/www && npm install && npm run start"
volumes:
- ./:/home/shlink/www
ports:

19359
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,132 +1,144 @@
{
"name": "shlink-web-client-react",
"name": "shlink-web-client",
"description": "A React-based progressive web application for shlink",
"version": "1.0.0",
"version": "2.3.0",
"private": false,
"homepage": "",
"repository": "https://github.com/shlinkio/shlink-web-client",
"license": "MIT",
"scripts": {
"lint": "yarn lint:js && yarn lint:css",
"lint": "npm run lint:js && npm run lint:css",
"lint:js": "eslint src test scripts config",
"lint:js:fix": "yarn lint:js --fix",
"lint:js:fix": "npm run lint:js -- --fix",
"lint:css": "stylelint src/*.scss src/**/*.scss",
"lint:css:fix": "yarn lint:css --fix",
"lint:css:fix": "npm run lint:css -- --fix",
"start": "node scripts/start.js",
"serve:build": "yarn serve ./build",
"serve:build": "serve ./build",
"build": "node scripts/build.js",
"test": "node scripts/test.js --env=jsdom --colors",
"test:ci": "yarn test --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=clover",
"test:pretty": "yarn test --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=html"
"test:ci": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=clover",
"test:pretty": "npm run test -- --coverage --coverageReporters=text --coverageReporters=text-summary --coverageReporters=html",
"mutate": "./node_modules/.bin/stryker run",
"mutate:ci": "npm run mutate -- --mutate=$MUTATION_FILES"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.6.3",
"@fortawesome/fontawesome-svg-core": "^1.2.0",
"@fortawesome/free-regular-svg-icons": "^5.6.3",
"@fortawesome/free-solid-svg-icons": "^5.6.3",
"@fortawesome/react-fontawesome": "^0.1.3",
"@fortawesome/fontawesome-free": "^5.11.2",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-regular-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
"@fortawesome/react-fontawesome": "^0.1.5",
"array-filter": "^1.0.0",
"array-map": "^0.0.0",
"array-reduce": "^0.0.0",
"axios": "^0.18.0",
"axios": "^0.19.0",
"bootstrap": "^4.3.1",
"bottlejs": "^1.7.1",
"chart.js": "^2.7.2",
"bottlejs": "^1.7.2",
"bowser": "^2.9.0",
"chart.js": "^2.8.0",
"classnames": "^2.2.6",
"compare-versions": "^3.5.1",
"csvjson": "^5.1.0",
"leaflet": "^1.4.0",
"moment": "^2.22.2",
"promise": "^8.0.1",
"prop-types": "^15.6.2",
"qs": "^6.5.2",
"leaflet": "^1.5.1",
"moment": "^2.24.0",
"promise": "^8.0.3",
"prop-types": "^15.7.2",
"qs": "^6.9.0",
"ramda": "^0.26.1",
"react": "^16.8.0",
"react-autosuggest": "^9.4.0",
"react-chartjs-2": "^2.7.4",
"react-color": "^2.14.1",
"react": "^16.13.1",
"react-autosuggest": "^9.4.3",
"react-chartjs-2": "^2.8.0",
"react-color": "^2.17.3",
"react-copy-to-clipboard": "^5.0.1",
"react-datepicker": "~1.5.0",
"react-dom": "^16.8.0",
"react-leaflet": "^2.2.1",
"react-moment": "^0.7.6",
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"react-swipeable": "^4.3.0",
"react-dom": "^16.13.1",
"react-external-link": "^1.0.0",
"react-leaflet": "^2.4.0",
"react-moment": "^0.9.5",
"react-redux": "^7.1.1",
"react-router-dom": "^5.1.2",
"react-swipeable": "^5.4.0",
"react-tagsinput": "^3.19.0",
"reactstrap": "^6.0.1",
"redux": "^4.0.0",
"reactstrap": "^8.0.1",
"redux": "^4.0.4",
"redux-actions": "^2.6.5",
"redux-thunk": "^2.3.0",
"uuid": "^3.3.2"
"uuid": "^3.3.3"
},
"devDependencies": {
"@babel/core": "^7.1.6",
"@svgr/webpack": "^2.4.1",
"adm-zip": "0.4.11",
"autoprefixer": "^7.1.6",
"@babel/core": "^7.6.2",
"@stryker-mutator/core": "^2.1.0",
"@stryker-mutator/html-reporter": "^2.1.0",
"@stryker-mutator/javascript-mutator": "^2.1.0",
"@stryker-mutator/jest-runner": "^2.1.0",
"@svgr/webpack": "^4.3.3",
"adm-zip": "^0.4.13",
"autoprefixer": "^9.6.3",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.4",
"babel-plugin-named-asset-import": "^0.3.0",
"babel-preset-react-app": "^7.0.0",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6",
"babel-plugin-named-asset-import": "^0.3.4",
"babel-preset-react-app": "^9.0.2",
"babel-runtime": "^6.26.0",
"bfj": "^6.1.1",
"case-sensitive-paths-webpack-plugin": "^2.1.2",
"chalk": "^2.4.1",
"css-loader": "^1.0.0",
"dotenv": "^6.0.0",
"dotenv-expand": "^4.2.0",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"bfj": "^7.0.1",
"case-sensitive-paths-webpack-plugin": "^2.2.0",
"chalk": "^2.4.2",
"css-loader": "^3.2.0",
"dotenv": "^8.1.0",
"dotenv-expand": "^5.1.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"eslint": "^5.11.1",
"eslint-config-adidas-babel": "^1.1.0",
"eslint-config-adidas-env": "^1.1.0",
"eslint-config-adidas-es6": "^1.2.0",
"eslint-config-adidas-react": "^1.1.1",
"eslint-loader": "^2.1.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jest": "^21.22.0",
"eslint-plugin-jsx-a11y": "^6.1.2",
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.11.1",
"file-loader": "^2.0.0",
"eslint-loader": "^3.0.2",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jest": "^22.17.0",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "^7.16.0",
"file-loader": "^4.2.0",
"fork-ts-checker-webpack-plugin-alt": "^0.4.14",
"fs-extra": "^7.0.0",
"html-webpack-plugin": "^4.0.0-alpha.2",
"fs-extra": "^8.1.0",
"html-webpack-plugin": "^4.0.0-beta.8",
"identity-obj-proxy": "^3.0.0",
"jest": "^23.6.0",
"jest-pnp-resolver": "^1.0.1",
"jest-resolve": "^23.6.0",
"mini-css-extract-plugin": "^0.4.3",
"node-sass": "^4.9.0",
"jest": "^24.9.0",
"jest-pnp-resolver": "^1.2.1",
"jest-resolve": "^24.9.0",
"mini-css-extract-plugin": "^0.8.0",
"node-sass": "^4.12.0",
"object-assign": "^4.1.1",
"ocular.js": "^0.1.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"pnp-webpack-plugin": "^1.1.0",
"postcss": "^7.0.7",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"pnp-webpack-plugin": "^1.5.0",
"postcss": "^7.0.18",
"postcss-flexbugs-fixes": "^4.1.0",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.3.1",
"postcss-preset-env": "^6.7.0",
"postcss-safe-parser": "^4.0.1",
"raf": "^3.4.0",
"react-app-polyfill": "^0.2.0",
"react-dev-utils": "^7.0.1",
"resolve": "^1.8.1",
"sass-loader": "^7.1.0",
"serve": "^10.0.0",
"sinon": "^6.1.5",
"style-loader": "^0.23.0",
"stylelint": "^9.9.0",
"raf": "^3.4.1",
"react-app-polyfill": "^1.0.4",
"react-dev-utils": "^9.1.0",
"resolve": "^1.12.0",
"sass-loader": "^8.0.0",
"serve": "^11.2.0",
"stryker-cli": "^1.0.0",
"style-loader": "^1.0.0",
"stylelint": "^9.10.1",
"stylelint-config-adidas": "^1.2.1",
"stylelint-config-adidas-bem": "^1.2.0",
"stylelint-config-recommended-scss": "^3.2.0",
"stylelint-scss": "^3.3.0",
"sw-precache-webpack-plugin": "^0.11.4",
"terser-webpack-plugin": "^1.1.0",
"url-loader": "^1.1.1",
"webpack": "^4.19.1",
"webpack-dev-server": "^3.1.14",
"webpack-manifest-plugin": "^2.0.4",
"whatwg-fetch": "^2.0.3",
"workbox-webpack-plugin": "^3.6.3"
"stylelint-config-recommended-scss": "^4.0.0",
"stylelint-scss": "^3.11.1",
"sw-precache-webpack-plugin": "^0.11.5",
"terser-webpack-plugin": "^2.1.2",
"url-loader": "^2.2.0",
"webpack": "^4.41.0",
"webpack-dev-server": "^3.8.2",
"webpack-manifest-plugin": "^2.2.0",
"whatwg-fetch": "^3.0.0",
"workbox-webpack-plugin": "^4.3.1"
},
"babel": {
"presets": [

16
public/.htaccess Normal file
View File

@@ -0,0 +1,16 @@
RewriteEngine on
RewriteBase /
# do not do anything for already existing files
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule (.*) - [L]
# if request is no valid file NOR directory
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# if static asset do not do anything
RewriteRule (.*)(css|js|html|png|jpe?g|gif|bmp|ico|json|csv|otf|eot|svg|svgz|ttf|woff|woff2|ijmap|pdf|tif|map) - [NC,L,R=404]
# everything else should be redirected to /index.html so it can be routed by it
RewriteRule (.*) /index.html [L]

BIN
public/favicon.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

1
public/favicon.svg Normal file
View File

@@ -0,0 +1 @@
<svg width="512pt" height="512pt" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg"><g fill="#4595e3"><path d=" M 23.71 85.08 C 17.22 49.81 49.44 14.86 85.08 18.12 C 118.83 19.21 145.72 53.33 139.45 86.37 C 155.64 102.30 171.32 118.83 187.87 134.36 C 198.32 111.73 208.84 89.12 219.57 66.62 C 226.05 53.84 243.47 48.74 255.73 56.27 C 263.76 62.10 270.34 69.69 277.25 76.75 C 286.28 86.61 285.72 102.89 276.31 112.31 C 223.38 165.37 170.38 218.37 117.35 271.34 C 107.72 280.99 91.01 281.25 81.11 271.86 C 74.39 264.94 66.82 258.69 61.24 250.77 C 53.72 238.52 58.85 221.07 71.64 214.62 C 94.11 203.87 116.72 193.38 139.33 182.91 C 123.81 166.36 107.30 150.68 91.37 134.49 C 60.20 140.28 27.37 116.78 23.71 85.08 Z" /><path d=" M 205.21 201.23 C 225.32 181.36 260.88 181.11 281.14 200.86 C 299.25 218.75 317.37 236.65 335.10 254.93 C 356.73 278.01 352.01 318.70 326.03 336.56 C 320.07 330.47 313.73 324.65 308.12 318.28 C 323.86 309.39 328.76 286.18 316.63 272.39 C 301.73 256.95 286.30 242.03 271.24 226.75 C 264.49 219.65 256.80 212.00 246.37 211.52 C 224.65 208.64 205.52 233.36 214.49 253.58 C 221.09 266.81 234.22 275.12 243.62 286.24 C 240.43 295.96 238.09 306.13 238.29 316.46 C 225.55 304.29 213.16 291.73 200.89 279.09 C 180.97 257.57 183.10 220.45 205.21 201.23 Z" /><path d=" M 273.90 352.07 C 252.28 328.99 256.98 288.31 282.96 270.46 C 288.93 276.54 295.26 282.36 300.88 288.72 C 285.14 297.62 280.23 320.82 292.38 334.61 C 307.27 350.05 322.70 364.96 337.75 380.25 C 344.51 387.35 352.20 395.00 362.64 395.48 C 384.35 398.37 403.49 373.64 394.51 353.42 C 387.92 340.18 374.78 331.88 365.38 320.76 C 368.56 311.04 370.91 300.86 370.71 290.54 C 383.45 302.70 395.84 315.27 408.11 327.91 C 428.03 349.43 425.90 386.55 403.78 405.77 C 383.68 425.64 348.13 425.89 327.86 406.14 C 309.75 388.25 291.60 370.37 273.90 352.07 Z" /><path d=" M 422.11 403.83 C 431.96 394.07 441.60 384.06 451.66 374.51 C 460.90 383.74 471.89 392.70 474.89 406.11 C 480.16 429.97 484.08 454.13 488.76 478.12 C 490.00 483.41 484.47 488.29 479.35 486.63 C 454.66 481.52 429.55 478.12 405.14 471.84 C 393.17 467.97 385.20 457.75 376.55 449.27 C 386.39 439.49 396.13 429.60 406.06 419.91 C 416.37 433.45 435.74 414.00 422.11 403.83 Z" /></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/icons/icon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/icons/icon-24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/icons/icon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
public/icons/icon-40x40.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

BIN
public/icons/icon-48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/icons/icon-60x60.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

BIN
public/icons/icon-64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 750 B

BIN
public/icons/icon-76x76.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 984 B

View File

@@ -11,12 +11,74 @@
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="apple-touch-icon" href="%PUBLIC_URL%/icons/shlink-128.png">
<link rel="apple-touch-icon" sizes="64x64" href="%PUBLIC_URL%/icons/shlink-64.png">
<link rel="apple-touch-icon" sizes="32x32" href="%PUBLIC_URL%/icons/shlink-32.png">
<link rel="apple-touch-icon" sizes="24x24" href="%PUBLIC_URL%/icons/shlink-24.png">
<link rel="apple-touch-icon" sizes="16x16" href="%PUBLIC_URL%/icons/shlink-16.png">
<!-- FavIcon itself -->
<link rel="icon" type="image/x-icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="icon" type="image/svg+xml" href="%PUBLIC_URL%/favicon.svg" sizes="any">
<link rel="icon" type="image/png" href="%PUBLIC_URL%/favicon.png">
<link rel="icon" type="image/gif" href="%PUBLIC_URL%/favicon.gif">
<!-- Apple Touch -->
<link rel="apple-touch-icon" sizes="16x16" href="%PUBLIC_URL%/icons/icon-16x16.png">
<link rel="apple-touch-icon" sizes="24x24" href="%PUBLIC_URL%/icons/icon-24x24.png">
<link rel="apple-touch-icon" sizes="32x32" href="%PUBLIC_URL%/icons/icon-32x32.png">
<link rel="apple-touch-icon" sizes="40x40" href="%PUBLIC_URL%/icons/icon-40x40.png">
<link rel="apple-touch-icon" sizes="48x48" href="%PUBLIC_URL%/icons/icon-48x48.png">
<link rel="apple-touch-icon" sizes="60x60" href="%PUBLIC_URL%/icons/icon-60x60.png">
<link rel="apple-touch-icon" sizes="64x64" href="%PUBLIC_URL%/icons/icon-64x64.png">
<link rel="apple-touch-icon" sizes="72x72" href="%PUBLIC_URL%/icons/icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="%PUBLIC_URL%/icons/icon-76x76.png">
<link rel="apple-touch-icon" sizes="96x96" href="%PUBLIC_URL%/icons/icon-96x96.png">
<link rel="apple-touch-icon" sizes="114x114" href="%PUBLIC_URL%/icons/icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="%PUBLIC_URL%/icons/icon-120x120.png">
<link rel="apple-touch-icon" sizes="128x128" href="%PUBLIC_URL%/icons/icon-128x128.png">
<link rel="apple-touch-icon" sizes="144x144" href="%PUBLIC_URL%/icons/icon-144x144.png">
<link rel="apple-touch-icon" sizes="150x150" href="%PUBLIC_URL%/icons/icon-150x150.png">
<link rel="apple-touch-icon" sizes="152x152" href="%PUBLIC_URL%/icons/icon-152x152.png">
<link rel="apple-touch-icon" sizes="160x160" href="%PUBLIC_URL%/icons/icon-160x160.png">
<link rel="apple-touch-icon" sizes="167x167" href="%PUBLIC_URL%/icons/icon-167x167.png">
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/icons/icon-180x180.png">
<link rel="apple-touch-icon" sizes="192x192" href="%PUBLIC_URL%/icons/icon-192x192.png">
<link rel="apple-touch-icon" sizes="196x196" href="%PUBLIC_URL%/icons/icon-196x196.png">
<link rel="apple-touch-icon" sizes="228x228" href="%PUBLIC_URL%/icons/icon-228x228.png">
<link rel="apple-touch-icon" sizes="256x256" href="%PUBLIC_URL%/icons/icon-256x256.png">
<link rel="apple-touch-icon" sizes="310x310" href="%PUBLIC_URL%/icons/icon-310x310.png">
<link rel="apple-touch-icon" sizes="384x384" href="%PUBLIC_URL%/icons/icon-384x384.png">
<link rel="apple-touch-icon" sizes="512x512" href="%PUBLIC_URL%/icons/icon-512x512.png">
<link rel="apple-touch-icon" sizes="1024x1024" href="%PUBLIC_URL%/icons/icon-1024x1024.png">
<!-- Normal -->
<link rel="icon" type="image/png" sizes="1024x1024" href="%PUBLIC_URL%/icons/icon-1024x1024.png">
<link rel="icon" type="image/png" sizes="512x512" href="%PUBLIC_URL%/icons/icon-512x512.png">
<link rel="icon" type="image/png" sizes="384x384" href="%PUBLIC_URL%/icons/icon-384x384.png">
<link rel="icon" type="image/png" sizes="310x310" href="%PUBLIC_URL%/icons/icon-310x310.png">
<link rel="icon" type="image/png" sizes="256x256" href="%PUBLIC_URL%/icons/icon-256x256.png">
<link rel="icon" type="image/png" sizes="228x228" href="%PUBLIC_URL%/icons/icon-228x228.png">
<link rel="icon" type="image/png" sizes="196x196" href="%PUBLIC_URL%/icons/icon-196x196.png">
<link rel="icon" type="image/png" sizes="192x192" href="%PUBLIC_URL%/icons/icon-192x192.png">
<link rel="icon" type="image/png" sizes="180x180" href="%PUBLIC_URL%/icons/icon-180x180.png">
<link rel="icon" type="image/png" sizes="167x167" href="%PUBLIC_URL%/icons/icon-167x167.png">
<link rel="icon" type="image/png" sizes="160x160" href="%PUBLIC_URL%/icons/icon-160x160.png">
<link rel="icon" type="image/png" sizes="152x152" href="%PUBLIC_URL%/icons/icon-152x152.png">
<link rel="icon" type="image/png" sizes="150x150" href="%PUBLIC_URL%/icons/icon-150x150.png">
<link rel="icon" type="image/png" sizes="144x144" href="%PUBLIC_URL%/icons/icon-144x144.png">
<link rel="icon" type="image/png" sizes="128x128" href="%PUBLIC_URL%/icons/icon-128x128.png">
<link rel="icon" type="image/png" sizes="120x120" href="%PUBLIC_URL%/icons/icon-120x120.png">
<link rel="icon" type="image/png" sizes="114x114" href="%PUBLIC_URL%/icons/icon-114x114.png">
<link rel="icon" type="image/png" sizes="96x96" href="%PUBLIC_URL%/icons/icon-96x96.png">
<link rel="icon" type="image/png" sizes="76x76" href="%PUBLIC_URL%/icons/icon-76x76.png">
<link rel="icon" type="image/png" sizes="72x72" href="%PUBLIC_URL%/icons/icon-72x72.png">
<link rel="icon" type="image/png" sizes="64x64" href="%PUBLIC_URL%/icons/icon-64x64.png">
<link rel="icon" type="image/png" sizes="60x60" href="%PUBLIC_URL%/icons/icon-60x60.png">
<link rel="icon" type="image/png" sizes="48x48" href="%PUBLIC_URL%/icons/icon-48x48.png">
<link rel="icon" type="image/png" sizes="40x40" href="%PUBLIC_URL%/icons/icon-40x40.png">
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/icons/icon-32x32.png">
<link rel="icon" type="image/png" sizes="24x24" href="%PUBLIC_URL%/icons/icon-24x24.png">
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/icons/icon-16x16.png">
<!-- MS -->
<meta name="msapplication-TileImage" content="%PUBLIC_URL%/icons/icon-144x144.png">
<meta name="msapplication-square70x70logo" content="%PUBLIC_URL%/icons/icon-70x70.png">
<meta name="msapplication-square144x144logo" content="%PUBLIC_URL%/icons/icon-144x144.png">
<meta name="msapplication-square150x150logo" content="%PUBLIC_URL%/icons/icon-150x150.png">
<meta name="msapplication-square310x310logo" content="%PUBLIC_URL%/icons/icon-310x310.png">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.

View File

@@ -6,16 +6,66 @@
"theme_color": "#4696e5",
"background_color": "#4696e5",
"icons": [
{
"src": "./icons/icon-16x16.png",
"type": "image/png",
"sizes": "16x16"
},
{
"src": "./icons/icon-24x24.png",
"type": "image/png",
"sizes": "24x24"
},
{
"src": "./icons/icon-32x32.png",
"type": "image/png",
"sizes": "32x32"
},
{
"src": "./icons/icon-40x40.png",
"type": "image/png",
"sizes": "40x40"
},
{
"src": "./icons/icon-48x48.png",
"type": "image/png",
"sizes": "48x48"
},
{
"src": "./icons/icon-60x60.png",
"type": "image/png",
"sizes": "60x60"
},
{
"src": "./icons/icon-64x64.png",
"type": "image/png",
"sizes": "64x64"
},
{
"src": "./icons/icon-72x72.png",
"type": "image/png",
"sizes": "72x72"
},
{
"src": "./icons/icon-76x76.png",
"type": "image/png",
"sizes": "76x76"
},
{
"src": "./icons/icon-96x96.png",
"type": "image/png",
"sizes": "96x96"
},
{
"src": "./icons/icon-114x114.png",
"type": "image/png",
"sizes": "114x114"
},
{
"src": "./icons/icon-120x120.png",
"type": "image/png",
"sizes": "120x120"
},
{
"src": "./icons/icon-128x128.png",
"type": "image/png",
@@ -26,20 +76,70 @@
"type": "image/png",
"sizes": "144x144"
},
{
"src": "./icons/icon-150x150.png",
"type": "image/png",
"sizes": "150x150"
},
{
"src": "./icons/icon-152x152.png",
"type": "image/png",
"sizes": "152x152"
},
{
"src": "./icons/icon-160x160.png",
"type": "image/png",
"sizes": "160x160"
},
{
"src": "./icons/icon-167x167.png",
"type": "image/png",
"sizes": "167x167"
},
{
"src": "./icons/icon-180x180.png",
"type": "image/png",
"sizes": "180x180"
},
{
"src": "./icons/icon-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "./icons/icon-196x196.png",
"type": "image/png",
"sizes": "196x196"
},
{
"src": "./icons/icon-228x228.png",
"type": "image/png",
"sizes": "228x228"
},
{
"src": "./icons/icon-256x256.png",
"type": "image/png",
"sizes": "256x256"
},
{
"src": "./icons/icon-310x310.png",
"type": "image/png",
"sizes": "310x310"
},
{
"src": "./icons/icon-384x384.png",
"type": "image/png",
"sizes": "384x384"
},
{
"src": "./icons/icon-512x512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "./icons/icon-1024x1024.png",
"type": "image/png",
"sizes": "1024x1024"
}
]
}

View File

@@ -14,7 +14,6 @@ process.on('unhandledRejection', (err) => {
// Ensure environment variables are read.
require('../config/env');
const path = require('path');
const chalk = require('chalk');
const fs = require('fs-extra');
const webpack = require('webpack');
@@ -22,7 +21,6 @@ const bfj = require('bfj');
const AdmZip = require('adm-zip');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
const printBuildError = require('react-dev-utils/printBuildError');
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
@@ -30,7 +28,6 @@ const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const { measureFileSizesBeforeBuild, printFileSizesAfterBuild } = FileSizeReporter;
const useYarn = fs.existsSync(paths.yarnLockFile);
// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; // eslint-disable-line
@@ -47,6 +44,8 @@ if (!checkRequiredFiles([ paths.appHtml, paths.appIndexJs ])) {
const argvSliceStart = 2;
const argv = process.argv.slice(argvSliceStart);
const writeStatsJson = argv.indexOf('--stats') !== -1;
const withoutDist = argv.indexOf('--no-dist') !== -1;
const { version, hasVersion } = getVersionFromArgs(argv);
// Generate configuration
const config = configFactory('production');
@@ -85,6 +84,7 @@ checkBrowsers(paths.appPath, isInteractive)
);
} else {
console.log(chalk.green('Compiled successfully.\n'));
hasVersion && replaceVersionPlaceholder(version);
}
console.log('File sizes after gzip:\n');
@@ -96,20 +96,6 @@ checkBrowsers(paths.appPath, isInteractive)
WARN_AFTER_CHUNK_GZIP_SIZE
);
console.log();
const appPackage = require(paths.appPackageJson);
const { publicUrl } = paths;
const { output: { publicPath } } = config;
const buildFolder = path.relative(process.cwd(), paths.appBuild);
printHostingInstructions(
appPackage,
publicUrl,
publicPath,
buildFolder,
useYarn
);
},
(err) => {
console.log(chalk.red('Failed to compile.\n'));
@@ -117,7 +103,7 @@ checkBrowsers(paths.appPath, isInteractive)
process.exit(1);
}
)
.then(zipDist)
.then(() => hasVersion && !withoutDist && zipDist(version))
.catch((err) => {
if (err && err.message) {
console.log(err.message);
@@ -200,15 +186,7 @@ function copyPublicFolder() {
});
}
function zipDist() {
const minArgsToContainVersion = 3;
// If no version was provided, do nothing
if (process.argv.length < minArgsToContainVersion) {
return;
}
const [ , , version ] = process.argv;
function zipDist(version) {
const versionFileName = `./dist/shlink-web-client_${version}_dist.zip`;
console.log(chalk.cyan(`Generating dist file for version ${chalk.bold(version)}...`));
@@ -226,4 +204,24 @@ function zipDist() {
console.log(chalk.red('An error occurred while generating dist file'));
console.log(e);
}
console.log();
}
function getVersionFromArgs(argv) {
const [ version ] = argv;
return { version, hasVersion: !!version };
}
function replaceVersionPlaceholder(version) {
const staticJsFilesPath = './build/static/js';
const versionPlaceholder = '%_VERSION_%';
const isMainFile = (file) => file.startsWith('main.') && file.endsWith('.js');
const [ mainJsFile ] = fs.readdirSync(staticJsFilesPath).filter(isMainFile);
const filePath = `${staticJsFilesPath}/${mainJsFile}`;
const fileContent = fs.readFileSync(filePath, 'utf-8');
const replaced = fileContent.replace(versionPlaceholder, version);
fs.writeFileSync(filePath, replaced, 'utf-8');
}

13
scripts/docker/build Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
set -e
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
if [[ -z $TRAVIS_TAG ]]; then
docker build -t shlinkio/shlink-web-client:latest .
docker push shlinkio/shlink-web-client:latest
else
docker build --build-arg VERSION=${TRAVIS_TAG#?} -t shlinkio/shlink-web-client:${TRAVIS_TAG#?} -t shlinkio/shlink-web-client:stable .
docker push shlinkio/shlink-web-client:${TRAVIS_TAG#?}
docker push shlinkio/shlink-web-client:stable
fi

View File

@@ -81,7 +81,7 @@ checkBrowsers(paths.appPath, isInteractive)
const urls = prepareUrls(protocol, HOST, port);
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler(webpack, config, appName, urls, useYarn);
const compiler = createCompiler({ webpack, config, appName, urls, useYarn });
// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;

BIN
shlink-web-client.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

View File

@@ -3,14 +3,15 @@ import { Route, Switch } from 'react-router-dom';
import './App.scss';
import NotFound from './common/NotFound';
const App = (MainHeader, Home, MenuLayout, CreateServer) => () => (
const App = (MainHeader, Home, MenuLayout, CreateServer, EditServer) => () => (
<div className="container-fluid app-container">
<MainHeader />
<div className="app">
<Switch>
<Route exact path="/server/create" component={CreateServer} />
<Route exact path="/" component={Home} />
<Route exact path="/server/create" component={CreateServer} />
<Route exact path="/server/:serverId/edit" component={EditServer} />
<Route path="/server/:serverId" component={MenuLayout} />
<Route component={NotFound} />
</Switch>

View File

@@ -1,16 +1,34 @@
import { faList as listIcon, faLink as createIcon, faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
import {
faList as listIcon,
faLink as createIcon,
faTags as tagsIcon,
faPen as editIcon,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
import { NavLink } from 'react-router-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import classNames from 'classnames';
import { serverType } from '../servers/prop-types';
import './AsideMenu.scss';
const defaultProps = {
className: '',
showOnMobile: false,
const AsideMenuItem = ({ children, to, className, ...rest }) => (
<NavLink
className={classNames('aside-menu__item', className)}
activeClassName="aside-menu__item--selected"
to={to}
{...rest}
>
{children}
</NavLink>
);
AsideMenuItem.propTypes = {
children: PropTypes.node.isRequired,
to: PropTypes.string.isRequired,
className: PropTypes.string,
};
const propTypes = {
selectedServer: serverType,
className: PropTypes.string,
@@ -20,43 +38,34 @@ const propTypes = {
const AsideMenu = (DeleteServerButton) => {
const AsideMenu = ({ selectedServer, className, showOnMobile }) => {
const serverId = selectedServer ? selectedServer.id : '';
const asideClass = classnames('aside-menu', className, {
const asideClass = classNames('aside-menu', className, {
'aside-menu--hidden': !showOnMobile,
});
const shortUrlsIsActive = (match, location) => location.pathname.match('/list-short-urls');
const buildPath = (suffix) => `/server/${serverId}${suffix}`;
return (
<aside className={asideClass}>
<nav className="nav flex-column aside-menu__nav">
<NavLink
className="aside-menu__item"
activeClassName="aside-menu__item--selected"
to={`/server/${serverId}/list-short-urls/1`}
isActive={shortUrlsIsActive}
>
<AsideMenuItem to={buildPath('/list-short-urls/1')} isActive={shortUrlsIsActive}>
<FontAwesomeIcon icon={listIcon} />
<span className="aside-menu__item-text">List short URLs</span>
</NavLink>
<NavLink
className="aside-menu__item"
activeClassName="aside-menu__item--selected"
to={`/server/${serverId}/create-short-url`}
>
</AsideMenuItem>
<AsideMenuItem to={buildPath('/create-short-url')}>
<FontAwesomeIcon icon={createIcon} flip="horizontal" />
<span className="aside-menu__item-text">Create short URL</span>
</NavLink>
<NavLink
className="aside-menu__item"
activeClassName="aside-menu__item--selected"
to={`/server/${serverId}/manage-tags`}
>
</AsideMenuItem>
<AsideMenuItem to={buildPath('/manage-tags')}>
<FontAwesomeIcon icon={tagsIcon} />
<span className="aside-menu__item-text">Manage tags</span>
</NavLink>
</AsideMenuItem>
<AsideMenuItem to={buildPath('/edit')} className="aside-menu__item--push">
<FontAwesomeIcon icon={editIcon} />
<span className="aside-menu__item-text">Edit this server</span>
</AsideMenuItem>
<DeleteServerButton
className="aside-menu__item aside-menu__item--danger"
textClassName="aside-menu__item-text"
server={selectedServer}
/>
</nav>
@@ -64,7 +73,6 @@ const AsideMenu = (DeleteServerButton) => {
);
};
AsideMenu.defaultProps = defaultProps;
AsideMenu.propTypes = propTypes;
return AsideMenu;

View File

@@ -67,6 +67,9 @@ $asideMenuMobileWidth: 280px;
.aside-menu__item--danger {
color: $dangerColor;
}
.aside-menu__item--push {
margin-top: auto;
}

View File

@@ -3,6 +3,7 @@ import * as PropTypes from 'prop-types';
import './ErrorHandler.scss';
import { Button } from 'reactstrap';
// FIXME Replace with typescript: (window, console)
const ErrorHandler = ({ location }, { error }) => class ErrorHandler extends React.Component {
static propTypes = {
children: PropTypes.node.isRequired,

View File

@@ -1,50 +1,35 @@
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React, { useEffect } from 'react';
import { isEmpty, values } from 'ramda';
import React from 'react';
import { Link } from 'react-router-dom';
import { ListGroup, ListGroupItem } from 'reactstrap';
import PropTypes from 'prop-types';
import './Home.scss';
import ServersListGroup from '../servers/ServersListGroup';
export default class Home extends React.Component {
static propTypes = {
resetSelectedServer: PropTypes.func,
servers: PropTypes.object,
};
const propTypes = {
resetSelectedServer: PropTypes.func,
servers: PropTypes.object,
};
componentDidMount() {
this.props.resetSelectedServer();
}
const Home = ({ resetSelectedServer, servers: { list, loading } }) => {
const servers = values(list);
const hasServers = !isEmpty(servers);
render() {
const servers = values(this.props.servers);
const hasServers = !isEmpty(servers);
useEffect(() => {
resetSelectedServer();
}, []);
return (
<div className="home">
<h1 className="home__title">Welcome to Shlink</h1>
<h5 className="home__intro">
{hasServers && <span>Please, select a server.</span>}
{!hasServers && <span>Please, <Link to="/server/create">add a server</Link>.</span>}
</h5>
return (
<div className="home">
<h1 className="home__title">Welcome to Shlink</h1>
<ServersListGroup servers={servers}>
{!loading && hasServers && <span>Please, select a server.</span>}
{!loading && !hasServers && <span>Please, <Link to="/server/create">add a server</Link>.</span>}
{loading && <span>Trying to load servers...</span>}
</ServersListGroup>
</div>
);
};
{hasServers && (
<ListGroup className="home__servers-list">
{servers.map(({ name, id }) => (
<ListGroupItem
key={id}
tag={Link}
to={`/server/${id}/list-short-urls/1`}
className="home__servers-item"
>
{name}
<FontAwesomeIcon icon={chevronIcon} className="home__servers-item-icon" />
</ListGroupItem>
))}
</ListGroup>
)}
</div>
);
}
}
Home.propTypes = propTypes;
export default Home;

View File

@@ -1,5 +1,4 @@
@import '../utils/base';
@import '../utils/mixins/vertical-align';
.home {
text-align: center;
@@ -17,21 +16,3 @@
font-size: 2.2rem;
}
}
.home__servers-list {
margin-top: 1rem;
width: 100%;
max-width: 400px;
}
.home__servers-item.home__servers-item {
text-align: left;
position: relative;
padding: .75rem 2.5rem .75rem 1rem;
}
.home__servers-item-icon {
@include vertical-align();
right: 1rem;
}

View File

@@ -1,112 +1,82 @@
import React from 'react';
import React, { useEffect } from 'react';
import { Route, Switch } from 'react-router-dom';
import Swipeable from 'react-swipeable';
import { Swipeable } from 'react-swipeable';
import { faBars as burgerIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classnames from 'classnames';
import classNames from 'classnames';
import * as PropTypes from 'prop-types';
import { serverType } from '../servers/prop-types';
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
import { useToggle } from '../utils/helpers/hooks';
import NotFound from './NotFound';
import './MenuLayout.scss';
const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits) =>
class MenuLayout extends React.Component {
static propTypes = {
match: PropTypes.object,
selectServer: PropTypes.func,
location: PropTypes.object,
selectedServer: serverType,
const propTypes = {
match: PropTypes.object,
location: PropTypes.object,
selectedServer: serverType,
};
const MenuLayout = (TagsList, ShortUrls, AsideMenu, CreateShortUrl, ShortUrlVisits, ShlinkVersions, ServerError) => {
const MenuLayoutComp = ({ match, location, selectedServer }) => {
const [ sidebarVisible, toggleSidebar, showSidebar, hideSidebar ] = useToggle();
const { params: { serverId } } = match;
useEffect(() => hideSidebar(), [ location ]);
if (selectedServer.serverNotReachable) {
return <ServerError type="not-reachable" />;
}
const burgerClasses = classNames('menu-layout__burger-icon', {
'menu-layout__burger-icon--active': sidebarVisible,
});
const swipeMenuIfNoModalExists = (callback) => () => {
if (document.querySelector('.modal')) {
return;
}
callback();
};
state = { showSideBar: false };
return (
<React.Fragment>
<FontAwesomeIcon icon={burgerIcon} className={burgerClasses} onClick={toggleSidebar} />
// FIXME Shouldn't use componentWillMount, but this code has to be run before children components are rendered
/* eslint react/no-deprecated: "off" */
componentWillMount() {
const { match, selectServer } = this.props;
const { params: { serverId } } = match;
selectServer(serverId);
}
componentDidUpdate(prevProps) {
const { location } = this.props;
// Hide sidebar when location changes
if (location !== prevProps.location) {
this.setState({ showSideBar: false });
}
}
render() {
const { selectedServer, match } = this.props;
const { params: { serverId } } = match;
const burgerClasses = classnames('menu-layout__burger-icon', {
'menu-layout__burger-icon--active': this.state.showSideBar,
});
const swipeMenuIfNoModalExists = (showSideBar) => () => {
if (document.querySelector('.modal')) {
return;
}
this.setState({ showSideBar });
};
return (
<React.Fragment>
<FontAwesomeIcon
icon={burgerIcon}
className={burgerClasses}
onClick={() => this.setState(({ showSideBar }) => ({ showSideBar: !showSideBar }))}
/>
<Swipeable
delta={40}
className="menu-layout__swipeable"
onSwipedLeft={swipeMenuIfNoModalExists(false)}
onSwipedRight={swipeMenuIfNoModalExists(true)}
>
<div className="row menu-layout__swipeable-inner">
<AsideMenu
className="col-lg-2 col-md-3"
selectedServer={selectedServer}
showOnMobile={this.state.showSideBar}
/>
<div
className="col-lg-10 offset-lg-2 col-md-9 offset-md-3"
onClick={() => this.setState({ showSideBar: false })}
>
<Swipeable
delta={40}
className="menu-layout__swipeable"
onSwipedLeft={swipeMenuIfNoModalExists(hideSidebar)}
onSwipedRight={swipeMenuIfNoModalExists(showSidebar)}
>
<div className="row menu-layout__swipeable-inner">
<AsideMenu className="col-lg-2 col-md-3" selectedServer={selectedServer} showOnMobile={sidebarVisible} />
<div className="col-lg-10 offset-lg-2 col-md-9 offset-md-3" onClick={() => hideSidebar()}>
<div className="menu-layout__container">
<Switch>
<Route exact path="/server/:serverId/list-short-urls/:page" component={ShortUrls} />
<Route exact path="/server/:serverId/create-short-url" component={CreateShortUrl} />
<Route exact path="/server/:serverId/short-code/:shortCode/visits" component={ShortUrlVisits} />
<Route exact path="/server/:serverId/manage-tags" component={TagsList} />
<Route
exact
path="/server/:serverId/list-short-urls/:page"
component={ShortUrls}
/>
<Route
exact
path="/server/:serverId/create-short-url"
component={CreateShortUrl}
/>
<Route
exact
path="/server/:serverId/short-code/:shortCode/visits"
component={ShortUrlVisits}
/>
<Route
exact
path="/server/:serverId/manage-tags"
component={TagsList}
/>
<Route
render={() => <NotFound to={`/server/${serverId}/list-short-urls/1`} btnText="List short URLs" />}
render={() => <NotFound to={`/server/${serverId}/list-short-urls/1`}>List short URLs</NotFound>}
/>
</Switch>
</div>
<div className="menu-layout__footer text-center text-md-right">
<ShlinkVersions />
</div>
</div>
</Swipeable>
</React.Fragment>
);
}
</div>
</Swipeable>
</React.Fragment>
);
};
MenuLayoutComp.propTypes = propTypes;
return withSelectedServer(MenuLayoutComp, ServerError);
};
export default MenuLayout;

View File

@@ -32,3 +32,26 @@
.menu-layout__burger-icon--active {
color: white;
}
$footer-height: 2.3rem;
$footer-margin: .8rem;
.menu-layout__container {
padding: 20px 0 ($footer-height + $footer-margin);
min-height: 100%;
margin-bottom: -($footer-height + $footer-margin);
@media (min-width: $mdMin) {
padding: 30px 15px ($footer-height + $footer-margin);
}
}
.menu-layout__footer {
height: $footer-height;
margin-top: $footer-margin;
padding: 0;
@media (min-width: $mdMin) {
padding: 0 15px;
}
}

View File

@@ -4,17 +4,18 @@ import * as PropTypes from 'prop-types';
const propTypes = {
to: PropTypes.string,
btnText: PropTypes.string,
children: PropTypes.node,
};
const NotFound = ({ to = '/', btnText = 'Home' }) => (
const NotFound = ({ to = '/', children = 'Home' }) => (
<div className="home">
<h2>Oops! We could not find requested route.</h2>
<p>
Use your browser{'\''}s back button to navigate to the page you have previously come from, or just press this button.
Use your browser&apos;s back button to navigate to the page you have previously come from, or just press this
button.
</p>
<br />
<Link to={to} className="btn btn-outline-primary btn-lg">{btnText}</Link>
<Link to={to} className="btn btn-outline-primary btn-lg">{children}</Link>
</div>
);

View File

@@ -0,0 +1,29 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { pipe } from 'ramda';
import { serverType } from '../servers/prop-types';
import { versionToPrintable, versionToSemVer } from '../utils/helpers/version';
const SHLINK_WEB_CLIENT_VERSION = '%_VERSION_%';
const propTypes = {
selectedServer: serverType,
className: PropTypes.string,
clientVersion: PropTypes.string,
};
const ShlinkVersions = ({ selectedServer, className, clientVersion = SHLINK_WEB_CLIENT_VERSION }) => {
const { printableVersion: serverVersion } = selectedServer;
const normalizedClientVersion = pipe(versionToSemVer(), versionToPrintable)(clientVersion);
return (
<small className={classNames('text-muted', className)}>
Client: <b>{normalizedClientVersion}</b> - Server: <b>{serverVersion}</b>
</small>
);
};
ShlinkVersions.propTypes = propTypes;
export default ShlinkVersions;

View File

@@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import { isPageDisabled, keyForPage, progressivePagination } from '../utils/helpers/pagination';
import './SimplePaginator.scss';
const propTypes = {
pagesCount: PropTypes.number.isRequired,
currentPage: PropTypes.number.isRequired,
setCurrentPage: PropTypes.func.isRequired,
centered: PropTypes.bool,
};
const SimplePaginator = ({ pagesCount, currentPage, setCurrentPage, centered = true }) => {
if (pagesCount < 2) {
return null;
}
const onClick = (page) => () => setCurrentPage(page);
return (
<Pagination listClassName={classNames('flex-wrap mb-0 simple-paginator', { 'justify-content-center': centered })}>
<PaginationItem disabled={currentPage <= 1}>
<PaginationLink previous tag="span" onClick={onClick(currentPage - 1)} />
</PaginationItem>
{progressivePagination(currentPage, pagesCount).map((pageNumber, index) => (
<PaginationItem
key={keyForPage(pageNumber, index)}
disabled={isPageDisabled(pageNumber)}
active={currentPage === pageNumber}
>
<PaginationLink tag="span" onClick={onClick(pageNumber)}>{pageNumber}</PaginationLink>
</PaginationItem>
))}
<PaginationItem disabled={currentPage >= pagesCount}>
<PaginationLink next tag="span" onClick={onClick(currentPage + 1)} />
</PaginationItem>
</Pagination>
);
};
SimplePaginator.propTypes = propTypes;
export default SimplePaginator;

View File

@@ -0,0 +1,3 @@
.simple-paginator {
user-select: none;
}

View File

@@ -4,6 +4,7 @@ import Home from '../Home';
import MenuLayout from '../MenuLayout';
import AsideMenu from '../AsideMenu';
import ErrorHandler from '../ErrorHandler';
import ShlinkVersions from '../ShlinkVersions';
const provideServices = (bottle, connect, withRouter) => {
bottle.constant('window', global.window);
@@ -25,13 +26,18 @@ const provideServices = (bottle, connect, withRouter) => {
'ShortUrls',
'AsideMenu',
'CreateShortUrl',
'ShortUrlVisits'
'ShortUrlVisits',
'ShlinkVersions',
'ServerError'
);
bottle.decorator('MenuLayout', connect([ 'selectedServer', 'shortUrlsListParams' ], [ 'selectServer' ]));
bottle.decorator('MenuLayout', withRouter);
bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton');
bottle.serviceFactory('ShlinkVersions', () => ShlinkVersions);
bottle.decorator('ShlinkVersions', connect([ 'selectedServer' ]));
bottle.serviceFactory('ErrorHandler', ErrorHandler, 'window', 'console');
};

View File

@@ -20,13 +20,13 @@ const mapActionService = (map, actionName) => ({
// Wrap actual action service in a function so that it is lazily created the first time it is called
[actionName]: lazyService(container, actionName),
});
const connect = (propsFromState, actionServiceNames) =>
const connect = (propsFromState, actionServiceNames = []) =>
reduxConnect(
propsFromState ? pick(propsFromState) : null,
actionServiceNames.reduce(mapActionService, {})
);
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer');
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer', 'EditServer');
provideCommonServices(bottle, connect, withRouter);
provideShortUrlsServices(bottle, connect);

View File

@@ -10,10 +10,6 @@ body,
outline: none !important;
}
.nowrap {
white-space: nowrap;
}
.bg-main {
background-color: $mainColor !important;
}
@@ -28,14 +24,6 @@ body,
color: inherit !important;
}
.shlink-container {
padding: 20px 0;
@media (min-width: $mdMin) {
padding: 30px 15px;
}
}
.badge-main {
color: #fff;
background-color: $mainColor;
@@ -59,3 +47,15 @@ body,
.paddingless {
padding: 0;
}
.indivisible {
white-space: nowrap;
}
.react-datepicker__day--keyboard-selected {
background-color: $mainColor;
&:hover {
background-color: darken($mainColor, 12%);
}
}

View File

@@ -6,6 +6,8 @@ import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListPara
import shortUrlCreationReducer from '../short-urls/reducers/shortUrlCreation';
import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion';
import shortUrlTagsReducer from '../short-urls/reducers/shortUrlTags';
import shortUrlMetaReducer from '../short-urls/reducers/shortUrlMeta';
import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition';
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
import shortUrlDetailReducer from '../visits/reducers/shortUrlDetail';
import tagsListReducer from '../tags/reducers/tagsList';
@@ -20,6 +22,8 @@ export default combineReducers({
shortUrlCreationResult: shortUrlCreationReducer,
shortUrlDeletion: shortUrlDeletionReducer,
shortUrlTags: shortUrlTagsReducer,
shortUrlMeta: shortUrlMetaReducer,
shortUrlEdition: shortUrlEditionReducer,
shortUrlVisits: shortUrlVisitsReducer,
shortUrlDetail: shortUrlDetailReducer,
tagsList: tagsListReducer,

View File

@@ -1,91 +1,56 @@
import { assoc, dissoc, pipe } from 'ramda';
import React from 'react';
import React, { useEffect } from 'react';
import { v4 as uuid } from 'uuid';
import PropTypes from 'prop-types';
import './CreateServer.scss';
import { ServerForm } from './helpers/ServerForm';
const SHOW_IMPORT_MSG_TIME = 4000;
const propTypes = {
createServer: PropTypes.func,
history: PropTypes.shape({
push: PropTypes.func,
}),
resetSelectedServer: PropTypes.func,
};
const CreateServer = (ImportServersBtn, stateFlagTimeout) => class CreateServer extends React.Component {
static propTypes = {
createServer: PropTypes.func,
history: PropTypes.shape({
push: PropTypes.func,
}),
resetSelectedServer: PropTypes.func,
};
const CreateServer = (ImportServersBtn, useStateFlagTimeout) => {
const CreateServerComp = ({ createServer, history: { push }, resetSelectedServer }) => {
const [ serversImported, setServersImported ] = useStateFlagTimeout(false, SHOW_IMPORT_MSG_TIME);
const handleSubmit = (serverData) => {
const id = uuid();
const server = { id, ...serverData };
state = {
name: '',
url: '',
apiKey: '',
serversImported: false,
};
createServer(server);
push(`/server/${id}/list-short-urls/1`);
};
handleSubmit = (e) => {
e.preventDefault();
const { createServer, history: { push } } = this.props;
const server = pipe(
assoc('id', uuid()),
dissoc('serversImported')
)(this.state);
createServer(server);
push(`/server/${server.id}/list-short-urls/1`);
};
componentDidMount() {
this.props.resetSelectedServer();
}
render() {
const renderInputGroup = (id, placeholder, type = 'text') => (
<div className="form-group row">
<label htmlFor={id} className="col-lg-1 col-md-2 col-form-label create-server__label">
{placeholder}:
</label>
<div className="col-lg-11 col-md-10">
<input
type={type}
className="form-control"
id={id}
placeholder={placeholder}
value={this.state[id]}
required
onChange={(e) => this.setState({ [id]: e.target.value })}
/>
</div>
</div>
);
useEffect(() => {
resetSelectedServer();
}, []);
return (
<div className="create-server">
<form onSubmit={this.handleSubmit}>
{renderInputGroup('name', 'Name')}
{renderInputGroup('url', 'URL', 'url')}
{renderInputGroup('apiKey', 'API key')}
<ServerForm onSubmit={handleSubmit}>
<ImportServersBtn onImport={setServersImported} />
<button className="btn btn-outline-primary">Create server</button>
</ServerForm>
<div className="text-right">
<ImportServersBtn
onImport={() => stateFlagTimeout(this.setState.bind(this), 'serversImported', true, SHOW_IMPORT_MSG_TIME)}
/>
<button className="btn btn-outline-primary">Create server</button>
</div>
{this.state.serversImported && (
<div className="row create-server__import-success-msg">
<div className="col-md-10 offset-md-1">
<div className="p-2 mt-3 bg-main text-white text-center">
Servers properly imported. You can now select one from the list :)
</div>
{serversImported && (
<div className="row create-server__import-success-msg">
<div className="col-md-10 offset-md-1">
<div className="p-2 mt-3 bg-main text-white text-center">
Servers properly imported. You can now select one from the list :)
</div>
</div>
)}
</form>
</div>
)}
</div>
);
}
};
CreateServerComp.propTypes = propTypes;
return CreateServerComp;
};
export default CreateServer;

View File

@@ -1,40 +1,36 @@
import React from 'react';
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
import PropTypes from 'prop-types';
import { useToggle } from '../utils/helpers/hooks';
import { serverType } from './prop-types';
const DeleteServerButton = (DeleteServerModal) => class DeleteServerButton extends React.Component {
static propTypes = {
server: serverType,
className: PropTypes.string,
};
const propTypes = {
server: serverType,
className: PropTypes.string,
textClassName: PropTypes.string,
children: PropTypes.node,
};
state = { isModalOpen: false };
render() {
const { server, className } = this.props;
const DeleteServerButton = (DeleteServerModal) => {
const DeleteServerButtonComp = ({ server, className, children, textClassName }) => {
const [ isModalOpen, , showModal, hideModal ] = useToggle();
return (
<React.Fragment>
<span
className={className}
key="deleteServerBtn"
onClick={() => this.setState({ isModalOpen: true })}
>
<FontAwesomeIcon icon={deleteIcon} />
<span className="aside-menu__item-text">Delete this server</span>
<span className={className} onClick={showModal}>
{!children && <FontAwesomeIcon icon={deleteIcon} />}
<span className={textClassName}>{children || 'Remove this server'}</span>
</span>
<DeleteServerModal
isOpen={this.state.isModalOpen}
toggle={() => this.setState(({ isModalOpen }) => ({ isModalOpen: !isModalOpen }))}
server={server}
key="deleteServerModal"
/>
<DeleteServerModal server={server} isOpen={isModalOpen} toggle={hideModal} />
</React.Fragment>
);
}
};
DeleteServerButtonComp.propTypes = propTypes;
return DeleteServerButtonComp;
};
export default DeleteServerButton;

View File

@@ -22,12 +22,14 @@ const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }) =>
return (
<Modal isOpen={isOpen} toggle={toggle} centered>
<ModalHeader toggle={toggle}><span className="text-danger">Delete server</span></ModalHeader>
<ModalHeader toggle={toggle}><span className="text-danger">Remove server</span></ModalHeader>
<ModalBody>
<p>Are you sure you want to delete server <b>{server ? server.name : ''}</b>?</p>
<p>Are you sure you want to remove <b>{server ? server.name : ''}</b>?</p>
<p>
No data will be deleted, only the access to that server will be removed from this host.
You can create it again at any moment.
<i>
No data will be deleted, only the access to this server will be removed from this host.
You can create it again at any moment.
</i>
</p>
</ModalBody>
<ModalFooter>

34
src/servers/EditServer.js Normal file
View File

@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ServerForm } from './helpers/ServerForm';
import { withSelectedServer } from './helpers/withSelectedServer';
import { serverType } from './prop-types';
const propTypes = {
editServer: PropTypes.func,
selectedServer: serverType,
history: PropTypes.shape({
push: PropTypes.func,
}),
};
export const EditServer = (ServerError) => {
const EditServerComp = ({ editServer, selectedServer, history: { push } }) => {
const handleSubmit = (serverData) => {
editServer(selectedServer.id, serverData);
push(`/server/${selectedServer.id}/list-short-urls/1`);
};
return (
<div className="create-server">
<ServerForm initialValues={selectedServer} onSubmit={handleSubmit}>
<button className="btn btn-outline-primary">Save</button>
</ServerForm>
</div>
);
};
EditServerComp.propTypes = propTypes;
return withSelectedServer(EditServerComp, ServerError);
};

View File

@@ -1,6 +1,5 @@
import { isEmpty, values } from 'ramda';
import React from 'react';
import { Link } from 'react-router-dom';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import PropTypes from 'prop-types';
import { serverType } from './prop-types';
@@ -9,12 +8,21 @@ const ServersDropdown = (serversExporter) => class ServersDropdown extends React
static propTypes = {
servers: PropTypes.object,
selectedServer: serverType,
selectServer: PropTypes.func,
listServers: PropTypes.func,
history: PropTypes.shape({
push: PropTypes.func,
}),
};
renderServers = () => {
const { servers, selectedServer, selectServer } = this.props;
const { servers: { list, loading }, selectedServer } = this.props;
const servers = values(list);
const { push } = this.props.history;
const loadServer = (id) => push(`/server/${id}/list-short-urls/1`);
if (loading) {
return <DropdownItem disabled><i>Trying to load servers...</i></DropdownItem>;
}
if (isEmpty(servers)) {
return <DropdownItem disabled><i>Add a server first...</i></DropdownItem>;
@@ -22,42 +30,27 @@ const ServersDropdown = (serversExporter) => class ServersDropdown extends React
return (
<React.Fragment>
{values(servers).map(({ name, id }) => (
<DropdownItem
key={id}
tag={Link}
to={`/server/${id}/list-short-urls/1`}
active={selectedServer && selectedServer.id === id}
// FIXME This should be implicit
onClick={() => selectServer(id)}
>
{servers.map(({ name, id }) => (
<DropdownItem key={id} active={selectedServer && selectedServer.id === id} onClick={() => loadServer(id)}>
{name}
</DropdownItem>
))}
<DropdownItem divider />
<DropdownItem
className="servers-dropdown__export-item"
onClick={() => serversExporter.exportServers()}
>
<DropdownItem className="servers-dropdown__export-item" onClick={() => serversExporter.exportServers()}>
Export servers
</DropdownItem>
</React.Fragment>
);
};
componentDidMount() {
this.props.listServers();
}
componentDidMount = this.props.listServers;
render() {
return (
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav caret>Servers</DropdownToggle>
<DropdownMenu right>{this.renderServers()}</DropdownMenu>
</UncontrolledDropdown>
);
}
render = () => (
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav caret>Servers</DropdownToggle>
<DropdownMenu right>{this.renderServers()}</DropdownMenu>
</UncontrolledDropdown>
);
};
export default ServersDropdown;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ListGroup, ListGroupItem } from 'reactstrap';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronRight as chevronIcon } from '@fortawesome/free-solid-svg-icons';
import { serverType } from './prop-types';
import './ServersListGroup.scss';
const propTypes = {
servers: PropTypes.arrayOf(serverType).isRequired,
children: PropTypes.node.isRequired,
};
const ServerListItem = ({ id, name }) => (
<ListGroupItem tag={Link} to={`/server/${id}/list-short-urls/1`} className="servers-list__server-item">
{name}
<FontAwesomeIcon icon={chevronIcon} className="servers-list__server-item-icon" />
</ListGroupItem>
);
ServerListItem.propTypes = {
id: PropTypes.string,
name: PropTypes.string,
};
const ServersListGroup = ({ servers, children }) => (
<React.Fragment>
<div className="container">
<h5>{children}</h5>
</div>
{servers.length > 0 && (
<ListGroup className="servers-list__list-group mt-md-3">
{servers.map(({ id, name }) => <ServerListItem key={id} id={id} name={name} />)}
</ListGroup>
)}
</React.Fragment>
);
ServersListGroup.propTypes = propTypes;
export default ServersListGroup;

View File

@@ -0,0 +1,18 @@
@import '../utils/mixins/vertical-align';
.servers-list__list-group {
width: 100%;
max-width: 400px;
}
.servers-list__server-item.servers-list__server-item {
text-align: left;
position: relative;
padding: .75rem 2.5rem .75rem 1rem;
}
.servers-list__server-item-icon {
@include vertical-align();
right: 1rem;
}

View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
import { serverType } from '../prop-types';
import { versionMatch } from '../../utils/helpers/version';
const propTypes = {
minVersion: PropTypes.string,
maxVersion: PropTypes.string,
selectedServer: serverType,
children: PropTypes.node.isRequired,
};
const ForServerVersion = ({ minVersion, maxVersion, selectedServer, children }) => {
if (!selectedServer) {
return null;
}
const { version } = selectedServer;
const matchesVersion = versionMatch(version, { maxVersion, minVersion });
if (!matchesVersion) {
return null;
}
return <React.Fragment>{children}</React.Fragment>;
};
ForServerVersion.propTypes = propTypes;
export default ForServerVersion;

View File

@@ -1,7 +1,5 @@
import React from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import { assoc, map } from 'ramda';
import { v4 as uuid } from 'uuid';
import PropTypes from 'prop-types';
const ImportServersBtn = (serversImporter) => class ImportServersBtn extends React.Component {
@@ -22,10 +20,8 @@ const ImportServersBtn = (serversImporter) => class ImportServersBtn extends Rea
render() {
const { importServersFromFile } = serversImporter;
const { onImport, createServers } = this.props;
const assocId = (server) => assoc('id', uuid(), server);
const onChange = ({ target }) =>
importServersFromFile(target.files[0])
.then(map(assocId))
.then(createServers)
.then(onImport)
.then(() => {

View File

@@ -0,0 +1,50 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import Message from '../../utils/Message';
import ServersListGroup from '../ServersListGroup';
import { serverType } from '../prop-types';
import './ServerError.scss';
const propTypes = {
servers: PropTypes.object,
selectedServer: serverType,
type: PropTypes.oneOf([ 'not-found', 'not-reachable' ]).isRequired,
};
export const ServerError = (DeleteServerButton) => {
const ServerErrorComp = ({ type, servers: { list }, selectedServer }) => (
<div className="server-error__container flex-column">
<div className="row w-100 mb-3 mb-md-5">
<Message type="error">
{type === 'not-found' && 'Could not find this Shlink server.'}
{type === 'not-reachable' && (
<React.Fragment>
<p>Oops! Could not connect to this Shlink server.</p>
Make sure you have internet connection, and the server is properly configured and on-line.
</React.Fragment>
)}
</Message>
</div>
<ServersListGroup servers={Object.values(list)}>
These are the Shlink servers currently configured. Choose one of
them or <Link to="/server/create">add a new one</Link>.
</ServersListGroup>
{type === 'not-reachable' && (
<div className="container mt-3 mt-md-5">
<h5>
Alternatively, if you think you may have miss-configured this server, you
can <DeleteServerButton server={selectedServer} className="server-error__delete-btn">remove it</DeleteServerButton> or&nbsp;
<Link to={`/server/${selectedServer.id}/edit`}>edit it</Link>.
</h5>
</div>
)}
</div>
);
ServerErrorComp.propTypes = propTypes;
return ServerErrorComp;
};

View File

@@ -0,0 +1,17 @@
@import '../../utils/base';
.server-error__container {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.server-error__delete-btn {
color: $dangerColor;
cursor: pointer;
}
.server-error__delete-btn:hover {
text-decoration: underline;
}

View File

@@ -0,0 +1,41 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { HorizontalFormGroup } from '../../utils/HorizontalFormGroup';
const propTypes = {
onSubmit: PropTypes.func.isRequired,
initialValues: PropTypes.shape({
name: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
apiKey: PropTypes.string.isRequired,
}),
children: PropTypes.node.isRequired,
};
export const ServerForm = ({ onSubmit, initialValues, children }) => {
const [ name, setName ] = useState('');
const [ url, setUrl ] = useState('');
const [ apiKey, setApiKey ] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({ name, url, apiKey });
};
useEffect(() => {
initialValues && setName(initialValues.name);
initialValues && setUrl(initialValues.url);
initialValues && setApiKey(initialValues.apiKey);
}, [ initialValues ]);
return (
<form onSubmit={handleSubmit}>
<HorizontalFormGroup value={name} onChange={setName}>Name</HorizontalFormGroup>
<HorizontalFormGroup type="url" value={url} onChange={setUrl}>URL</HorizontalFormGroup>
<HorizontalFormGroup value={apiKey} onChange={setApiKey}>API key</HorizontalFormGroup>
<div className="text-right">{children}</div>
</form>
);
};
ServerForm.propTypes = propTypes;

View File

@@ -0,0 +1,35 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import Message from '../../utils/Message';
import { serverType } from '../prop-types';
const propTypes = {
selectServer: PropTypes.func,
selectedServer: serverType,
match: PropTypes.object,
};
export const withSelectedServer = (WrappedComponent, ServerError) => {
const Component = (props) => {
const { selectServer, selectedServer, match } = props;
const { params: { serverId } } = match;
useEffect(() => {
selectServer(serverId);
}, [ serverId ]);
if (!selectedServer) {
return <Message loading />;
}
if (selectedServer.serverNotFound) {
return <ServerError type="not-found" />;
}
return <WrappedComponent {...props} />;
};
Component.propTypes = propTypes;
return Component;
};

View File

@@ -1,8 +1,20 @@
import PropTypes from 'prop-types';
export const serverType = PropTypes.shape({
const regularServerType = PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
url: PropTypes.string,
apiKey: PropTypes.string,
version: PropTypes.string,
printableVersion: PropTypes.string,
serverNotReachable: PropTypes.bool,
});
const notFoundServerType = PropTypes.shape({
serverNotFound: PropTypes.bool.isRequired,
});
export const serverType = PropTypes.oneOfType([
regularServerType,
notFoundServerType,
]);

View File

@@ -1,32 +1,65 @@
import { createAction, handleActions } from 'redux-actions';
import { identity, memoizeWith, pipe } from 'ramda';
import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams';
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
/* eslint-disable padding-line-between-statements, newline-after-var */
/* eslint-disable padding-line-between-statements */
export const SELECT_SERVER = 'shlink/selectedServer/SELECT_SERVER';
export const RESET_SELECTED_SERVER = 'shlink/selectedServer/RESET_SELECTED_SERVER';
/* eslint-enable padding-line-between-statements, newline-after-var */
const defaultState = null;
export const MIN_FALLBACK_VERSION = '1.0.0';
export const MAX_FALLBACK_VERSION = '999.999.999';
export const LATEST_VERSION_CONSTRAINT = 'latest';
/* eslint-enable padding-line-between-statements */
export default function reducer(state = defaultState, action) {
switch (action.type) {
case SELECT_SERVER:
return action.selectedServer;
case RESET_SELECTED_SERVER:
return defaultState;
default:
return state;
}
}
const initialState = null;
const versionToSemVer = pipe(
(version) => version === LATEST_VERSION_CONSTRAINT ? MAX_FALLBACK_VERSION : version,
toSemVer(MIN_FALLBACK_VERSION)
);
export const resetSelectedServer = () => ({ type: RESET_SELECTED_SERVER });
const getServerVersion = memoizeWith(identity, (serverId, health) => health().then(({ version }) => ({
version: versionToSemVer(version),
printableVersion: versionToPrintable(version),
})));
export const selectServer = (serversService) => (serverId) => (dispatch) => {
export const resetSelectedServer = createAction(RESET_SELECTED_SERVER);
export const selectServer = ({ findServerById }, buildShlinkApiClient) => (serverId) => async (dispatch) => {
dispatch(resetSelectedServer());
dispatch(resetShortUrlParams());
const selectedServer = findServerById(serverId);
const selectedServer = serversService.findServerById(serverId);
if (!selectedServer) {
dispatch({
type: SELECT_SERVER,
selectedServer: { serverNotFound: true },
});
dispatch({
type: SELECT_SERVER,
selectedServer,
});
return;
}
try {
const { health } = buildShlinkApiClient(selectedServer);
const { version, printableVersion } = await getServerVersion(serverId, health);
dispatch({
type: SELECT_SERVER,
selectedServer: {
...selectedServer,
version,
printableVersion,
},
});
} catch (e) {
dispatch({
type: SELECT_SERVER,
selectedServer: { ...selectedServer, serverNotReachable: true },
});
}
};
export default handleActions({
[RESET_SELECTED_SERVER]: () => initialState,
[SELECT_SERVER]: (state, { selectedServer }) => selectedServer,
}, initialState);

View File

@@ -1,33 +1,63 @@
import { handleActions } from 'redux-actions';
import { pipe, isEmpty, assoc, map, prop } from 'ramda';
import { v4 as uuid } from 'uuid';
import { homepage } from '../../../package.json';
/* eslint-disable padding-line-between-statements */
export const FETCH_SERVERS_START = 'shlink/servers/FETCH_SERVERS_START';
export const FETCH_SERVERS = 'shlink/servers/FETCH_SERVERS';
/* eslint-enable padding-line-between-statements */
export default function reducer(state = {}, action) {
switch (action.type) {
case FETCH_SERVERS:
return action.servers;
default:
return state;
const initialState = {
list: {},
loading: false,
};
const assocId = (server) => assoc('id', server.id || uuid(), server);
export default handleActions({
[FETCH_SERVERS_START]: (state) => ({ ...state, loading: true }),
[FETCH_SERVERS]: (state, { list }) => ({ list, loading: false }),
}, initialState);
export const listServers = ({ listServers, createServers }, { get }) => () => async (dispatch) => {
dispatch({ type: FETCH_SERVERS_START });
const localList = listServers();
if (!isEmpty(localList)) {
dispatch({ type: FETCH_SERVERS, list: localList });
return;
}
}
export const listServers = (serversService) => () => ({
type: FETCH_SERVERS,
servers: serversService.listServers(),
});
// If local list is empty, try to fetch it remotely (making sure it's an array) and calculate IDs for every server
const getDataAsArrayWithIds = pipe(
prop('data'),
(value) => {
if (!Array.isArray(value)) {
throw new Error('Value is not an array');
}
export const createServer = (serversService, listServers) => (server) => {
serversService.createServer(server);
return value;
},
map(assocId),
);
const remoteList = await get(`${homepage}/servers.json`)
.then(getDataAsArrayWithIds)
.catch(() => []);
return listServers();
createServers(remoteList);
dispatch({ type: FETCH_SERVERS, list: remoteList.reduce((map, server) => ({ ...map, [server.id]: server }), {}) });
};
export const deleteServer = (serversService, listServers) => (server) => {
serversService.deleteServer(server);
export const createServer = ({ createServer }, listServersAction) => pipe(createServer, listServersAction);
return listServers();
};
export const editServer = ({ editServer }, listServersAction) => pipe(editServer, listServersAction);
export const createServers = (serversService, listServers) => (servers) => {
serversService.createServers(servers);
export const deleteServer = ({ deleteServer }, listServersAction) => pipe(deleteServer, listServersAction);
return listServers();
};
export const createServers = ({ createServers }, listServersAction) => pipe(
map(assocId),
createServers,
listServersAction
);

View File

@@ -1,9 +1,3 @@
import PropTypes from 'prop-types';
export const serversImporterType = PropTypes.shape({
importServersFromFile: PropTypes.func,
});
export default class ServersImporter {
constructor(csvjson) {
this.csvjson = csvjson;

View File

@@ -23,9 +23,16 @@ export default class ServersService {
this.storage.set(SERVERS_STORAGE_KEY, allServers);
};
deleteServer = (server) =>
this.storage.set(
SERVERS_STORAGE_KEY,
dissoc(server.id, this.listServers())
);
deleteServer = ({ id }) =>
this.storage.set(SERVERS_STORAGE_KEY, dissoc(id, this.listServers()));
editServer = (id, serverData) => {
const allServers = this.listServers();
if (!allServers[id]) {
return;
}
this.storage.set(SERVERS_STORAGE_KEY, assoc(id, { ...allServers[id], ...serverData }, allServers));
}
}

View File

@@ -3,20 +3,27 @@ import CreateServer from '../CreateServer';
import ServersDropdown from '../ServersDropdown';
import DeleteServerModal from '../DeleteServerModal';
import DeleteServerButton from '../DeleteServerButton';
import { EditServer } from '../EditServer';
import ImportServersBtn from '../helpers/ImportServersBtn';
import { resetSelectedServer, selectServer } from '../reducers/selectedServer';
import { createServer, createServers, deleteServer, listServers } from '../reducers/server';
import { createServer, createServers, deleteServer, editServer, listServers } from '../reducers/server';
import ForServerVersion from '../helpers/ForServerVersion';
import { ServerError } from '../helpers/ServerError';
import ServersImporter from './ServersImporter';
import ServersService from './ServersService';
import ServersExporter from './ServersExporter';
const provideServices = (bottle, connect, withRouter) => {
// Components
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'stateFlagTimeout');
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn', 'useStateFlagTimeout');
bottle.decorator('CreateServer', connect([ 'selectedServer' ], [ 'createServer', 'resetSelectedServer' ]));
bottle.serviceFactory('EditServer', EditServer, 'ServerError');
bottle.decorator('EditServer', connect([ 'selectedServer' ], [ 'editServer', 'selectServer' ]));
bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter');
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ], [ 'listServers', 'selectServer' ]));
bottle.decorator('ServersDropdown', withRouter);
bottle.decorator('ServersDropdown', connect([ 'servers', 'selectedServer' ], [ 'listServers' ]));
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
bottle.decorator('DeleteServerModal', withRouter);
@@ -27,6 +34,12 @@ const provideServices = (bottle, connect, withRouter) => {
bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter');
bottle.decorator('ImportServersBtn', connect(null, [ 'createServers' ]));
bottle.serviceFactory('ForServerVersion', () => ForServerVersion);
bottle.decorator('ForServerVersion', connect([ 'selectedServer' ]));
bottle.serviceFactory('ServerError', ServerError, 'DeleteServerButton');
bottle.decorator('ServerError', connect([ 'servers', 'selectedServer' ]));
// Services
bottle.constant('csvjson', csvjson);
bottle.service('ServersImporter', ServersImporter, 'csvjson');
@@ -34,11 +47,12 @@ const provideServices = (bottle, connect, withRouter) => {
bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson');
// Actions
bottle.serviceFactory('selectServer', selectServer, 'ServersService');
bottle.serviceFactory('selectServer', selectServer, 'ServersService', 'buildShlinkApiClient');
bottle.serviceFactory('createServer', createServer, 'ServersService', 'listServers');
bottle.serviceFactory('createServers', createServers, 'ServersService', 'listServers');
bottle.serviceFactory('deleteServer', deleteServer, 'ServersService', 'listServers');
bottle.serviceFactory('listServers', listServers, 'ServersService');
bottle.serviceFactory('editServer', editServer, 'ServersService', 'listServers');
bottle.serviceFactory('listServers', listServers, 'ServersService', 'axios');
bottle.serviceFactory('resetSelectedServer', () => resetSelectedServer);
};

View File

@@ -1,123 +1,173 @@
import { faAngleDoubleDown as downIcon, faAngleDoubleUp as upIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { assoc, dissoc, isNil, pipe, replace, trim } from 'ramda';
import React from 'react';
import { Collapse } from 'reactstrap';
import { isEmpty, isNil, pipe, replace, trim } from 'ramda';
import React, { useState } from 'react';
import { Collapse, FormGroup, Input } from 'reactstrap';
import * as PropTypes from 'prop-types';
import DateInput from '../utils/DateInput';
import Checkbox from '../utils/Checkbox';
import { serverType } from '../servers/prop-types';
import { versionMatch } from '../utils/helpers/version';
import { hasValue } from '../utils/utils';
import { useToggle } from '../utils/helpers/hooks';
import { createShortUrlResultType } from './reducers/shortUrlCreation';
import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
const normalizeTag = pipe(trim, replace(/ /g, '-'));
const formatDate = (date) => isNil(date) ? date : date.format();
const CreateShortUrl = (TagsSelector, CreateShortUrlResult) => class CreateShortUrl extends React.Component {
static propTypes = {
createShortUrl: PropTypes.func,
shortUrlCreationResult: createShortUrlResultType,
resetCreateShortUrl: PropTypes.func,
};
const propTypes = {
createShortUrl: PropTypes.func,
shortUrlCreationResult: createShortUrlResultType,
resetCreateShortUrl: PropTypes.func,
selectedServer: serverType,
};
state = {
longUrl: '',
tags: [],
customSlug: undefined,
validSince: undefined,
validUntil: undefined,
maxVisits: undefined,
moreOptionsVisible: false,
};
const initialState = {
longUrl: '',
tags: [],
customSlug: '',
shortCodeLength: '',
domain: '',
validSince: undefined,
validUntil: undefined,
maxVisits: '',
findIfExists: false,
};
render() {
const { createShortUrl, shortUrlCreationResult, resetCreateShortUrl } = this.props;
const CreateShortUrl = (TagsSelector, CreateShortUrlResult, ForServerVersion) => {
const CreateShortUrlComp = ({ createShortUrl, shortUrlCreationResult, resetCreateShortUrl, selectedServer }) => {
const [ shortUrlCreation, setShortUrlCreation ] = useState(initialState);
const [ moreOptionsVisible, toggleMoreOptionsVisible ] = useToggle();
const changeTags = (tags) => this.setState({ tags: tags.map(normalizeTag) });
const changeTags = (tags) => setShortUrlCreation({ ...shortUrlCreation, tags: tags.map(normalizeTag) });
const reset = () => setShortUrlCreation(initialState);
const save = (e) => {
e.preventDefault();
const shortUrlData = {
...shortUrlCreation,
validSince: formatDate(shortUrlCreation.validSince),
validUntil: formatDate(shortUrlCreation.validUntil),
};
createShortUrl(shortUrlData).then(reset).catch(() => {});
};
const renderOptionalInput = (id, placeholder, type = 'text', props = {}) => (
<div className="form-group">
<input
className="form-control"
<FormGroup>
<Input
id={id}
type={type}
placeholder={placeholder}
value={this.state[id]}
onChange={(e) => this.setState({ [id]: e.target.value })}
value={shortUrlCreation[id]}
onChange={(e) => setShortUrlCreation({ ...shortUrlCreation, [id]: e.target.value })}
{...props}
/>
</div>
</FormGroup>
);
const renderDateInput = (id, placeholder, props = {}) => (
<div className="form-group">
<DateInput
selected={this.state[id]}
selected={shortUrlCreation[id]}
placeholderText={placeholder}
isClearable
onChange={(date) => this.setState({ [id]: date })}
onChange={(date) => setShortUrlCreation({ ...shortUrlCreation, [id]: date })}
{...props}
/>
</div>
);
const save = (e) => {
e.preventDefault();
createShortUrl(pipe(
dissoc('moreOptionsVisible'),
assoc('validSince', formatDate(this.state.validSince)),
assoc('validUntil', formatDate(this.state.validUntil))
)(this.state));
};
const currentServerVersion = selectedServer && selectedServer.version;
const disableDomain = !versionMatch(currentServerVersion, { minVersion: '1.19.0-beta.1' });
const disableShortCodeLength = !versionMatch(currentServerVersion, { minVersion: '2.1.0' });
return (
<div className="shlink-container">
<form onSubmit={save}>
<form onSubmit={save}>
<div className="form-group">
<input
className="form-control form-control-lg"
type="url"
placeholder="Insert the URL to be shortened"
required
value={shortUrlCreation.longUrl}
onChange={(e) => setShortUrlCreation({ ...shortUrlCreation, longUrl: e.target.value })}
/>
</div>
<Collapse isOpen={moreOptionsVisible}>
<div className="form-group">
<input
className="form-control form-control-lg"
type="url"
placeholder="Insert the URL to be shortened"
required
value={this.state.longUrl}
onChange={(e) => this.setState({ longUrl: e.target.value })}
/>
<TagsSelector tags={shortUrlCreation.tags} onChange={changeTags} />
</div>
<Collapse isOpen={this.state.moreOptionsVisible}>
<div className="form-group">
<TagsSelector tags={this.state.tags} onChange={changeTags} />
<div className="row">
<div className="col-sm-4">
{renderOptionalInput('customSlug', 'Custom slug')}
</div>
<div className="row">
<div className="col-sm-6">
{renderOptionalInput('customSlug', 'Custom slug')}
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
</div>
<div className="col-sm-6">
{renderDateInput('validSince', 'Enabled since...', { maxDate: this.state.validUntil })}
{renderDateInput('validUntil', 'Enabled until...', { minDate: this.state.validSince })}
</div>
<div className="col-sm-4">
{renderOptionalInput('shortCodeLength', 'Short code length', 'number', {
min: 4,
disabled: disableShortCodeLength || hasValue(shortUrlCreation.customSlug),
...disableShortCodeLength && {
title: 'Shlink 2.1.0 or higher is required to be able to provide the short code length',
},
})}
</div>
<div className="col-sm-4">
{renderOptionalInput('domain', 'Domain', 'text', {
disabled: disableDomain,
...disableDomain && { title: 'Shlink 1.19.0 or higher is required to be able to provide the domain' },
})}
</div>
</Collapse>
<div>
<button
type="button"
className="btn btn-outline-secondary create-short-url__btn"
onClick={() => this.setState(({ moreOptionsVisible }) => ({ moreOptionsVisible: !moreOptionsVisible }))}
>
<FontAwesomeIcon icon={this.state.moreOptionsVisible ? upIcon : downIcon} />
&nbsp;
{this.state.moreOptionsVisible ? 'Less' : 'More'} options
</button>
<button
className="btn btn-outline-primary create-short-url__btn float-right"
disabled={shortUrlCreationResult.loading}
>
{shortUrlCreationResult.loading ? 'Creating...' : 'Create'}
</button>
</div>
<CreateShortUrlResult {...shortUrlCreationResult} resetCreateShortUrl={resetCreateShortUrl} />
</form>
</div>
<div className="row">
<div className="col-sm-4">
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
</div>
<div className="col-sm-4">
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlCreation.validUntil })}
</div>
<div className="col-sm-4">
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlCreation.validSince })}
</div>
</div>
<ForServerVersion minVersion="1.16.0">
<div className="mb-4 text-right">
<Checkbox
className="mr-2"
checked={shortUrlCreation.findIfExists}
onChange={(findIfExists) => setShortUrlCreation({ ...shortUrlCreation, findIfExists })}
>
Use existing URL if found
</Checkbox>
<UseExistingIfFoundInfoIcon />
</div>
</ForServerVersion>
</Collapse>
<div>
<button type="button" className="btn btn-outline-secondary" onClick={toggleMoreOptionsVisible}>
<FontAwesomeIcon icon={moreOptionsVisible ? upIcon : downIcon} />
&nbsp;
{moreOptionsVisible ? 'Less' : 'More'} options
</button>
<button
className="btn btn-outline-primary float-right"
disabled={shortUrlCreationResult.saving || isEmpty(shortUrlCreation.longUrl)}
>
{shortUrlCreationResult.saving ? 'Creating...' : 'Create'}
</button>
</div>
<CreateShortUrlResult {...shortUrlCreationResult} resetCreateShortUrl={resetCreateShortUrl} />
</form>
);
}
};
CreateShortUrlComp.propTypes = propTypes;
return CreateShortUrlComp;
};
export default CreateShortUrl;

View File

@@ -2,7 +2,8 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import PropTypes from 'prop-types';
import { rangeOf } from '../utils/utils';
import { isPageDisabled, keyForPage, progressivePagination } from '../utils/helpers/pagination';
import './Paginator.scss';
const propTypes = {
serverId: PropTypes.string.isRequired,
@@ -12,7 +13,7 @@ const propTypes = {
}),
};
export default function Paginator({ paginator = {}, serverId }) {
const Paginator = ({ paginator = {}, serverId }) => {
const { currentPage, pagesCount = 0 } = paginator;
if (pagesCount <= 1) {
@@ -20,8 +21,12 @@ export default function Paginator({ paginator = {}, serverId }) {
}
const renderPages = () =>
rangeOf(pagesCount, (pageNumber) => (
<PaginationItem key={pageNumber} active={currentPage === pageNumber}>
progressivePagination(currentPage, pagesCount).map((pageNumber, index) => (
<PaginationItem
key={keyForPage(pageNumber, index)}
disabled={isPageDisabled(pageNumber)}
active={currentPage === pageNumber}
>
<PaginationLink
tag={Link}
to={`/server/${serverId}/list-short-urls/${pageNumber}`}
@@ -32,7 +37,7 @@ export default function Paginator({ paginator = {}, serverId }) {
));
return (
<Pagination listClassName="flex-wrap">
<Pagination className="short-urls-paginator" listClassName="flex-wrap justify-content-center mb-0">
<PaginationItem disabled={currentPage === 1}>
<PaginationLink
previous
@@ -50,6 +55,8 @@ export default function Paginator({ paginator = {}, serverId }) {
</PaginationItem>
</Pagination>
);
}
};
Paginator.propTypes = propTypes;
export default Paginator;

View File

@@ -0,0 +1,7 @@
.short-urls-paginator {
position: sticky;
bottom: 0;
background-color: rgba(white, .8);
padding: .75rem 0;
border-top: 1px solid rgba(black, .125);
}

View File

@@ -1,10 +1,13 @@
import { faTags as tagsIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';
import { isEmpty } from 'ramda';
import { isEmpty, pipe } from 'ramda';
import PropTypes from 'prop-types';
import moment from 'moment';
import SearchField from '../utils/SearchField';
import Tag from '../tags/helpers/Tag';
import DateRangeRow from '../utils/DateRangeRow';
import { formatDate } from '../utils/helpers/date';
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
import './SearchBar.scss';
@@ -13,19 +16,41 @@ const propTypes = {
shortUrlsListParams: shortUrlsListParamsType,
};
const SearchBar = (colorGenerator) => {
const dateOrUndefined = (date) => date ? moment(date) : undefined;
const SearchBar = (colorGenerator, ForServerVersion) => {
const SearchBar = ({ listShortUrls, shortUrlsListParams }) => {
const selectedTags = shortUrlsListParams.tags || [];
const setDate = (dateName) => pipe(
formatDate(),
(date) => listShortUrls({ ...shortUrlsListParams, [dateName]: date })
);
return (
<div className="serach-bar-container">
<SearchField onChange={
(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })
}
<div className="search-bar-container">
<SearchField
onChange={
(searchTerm) => listShortUrls({ ...shortUrlsListParams, searchTerm })
}
/>
<ForServerVersion minVersion="1.21.0">
<div className="mt-3">
<div className="row">
<div className="col-lg-8 offset-lg-4 col-xl-6 offset-xl-6">
<DateRangeRow
startDate={dateOrUndefined(shortUrlsListParams.startDate)}
endDate={dateOrUndefined(shortUrlsListParams.endDate)}
onStartDateChange={setDate('startDate')}
onEndDateChange={setDate('endDate')}
/>
</div>
</div>
</div>
</ForServerVersion>
{!isEmpty(selectedTags) && (
<h4 className="search-bar__selected-tag mt-2">
<h4 className="search-bar__selected-tag mt-3">
<FontAwesomeIcon icon={tagsIcon} className="search-bar__tags-icon" />
&nbsp;
{selectedTags.map((tag) => (

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import Paginator from './Paginator';
@@ -14,16 +14,22 @@ const ShortUrls = (SearchBar, ShortUrlsList) => {
const { match: { params }, shortUrlsList } = props;
const { page, serverId } = params;
const { data = [], pagination } = shortUrlsList;
const [ urlsListKey, setUrlsListKey ] = useState(`${serverId}_${page}`);
// Using a key on a component makes react to create a new instance every time the key changes
const urlsListKey = `${serverId}_${page}`;
// Without it, pagination on the URL will not make the component to be refreshed
useEffect(() => {
setUrlsListKey(`${serverId}_${page}`);
}, [ serverId, page ]);
return (
<div className="shlink-container">
<React.Fragment>
<div className="form-group"><SearchBar /></div>
<ShortUrlsList {...props} shortUrlsList={data} key={urlsListKey} />
<Paginator paginator={pagination} serverId={serverId} />
</div>
<div>
<ShortUrlsList {...props} shortUrlsList={data} key={urlsListKey} />
<Paginator paginator={pagination} serverId={serverId} />
</div>
</React.Fragment>
);
};

View File

@@ -11,13 +11,14 @@ import { shortUrlType } from './reducers/shortUrlsList';
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
import './ShortUrlsList.scss';
const SORTABLE_FIELDS = {
export const SORTABLE_FIELDS = {
dateCreated: 'Created at',
shortCode: 'Short URL',
longUrl: 'Long URL',
visits: 'Visits',
};
// FIXME Replace with typescript: (ShortUrlsRow component)
const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Component {
static propTypes = {
listShortUrls: PropTypes.func,
@@ -39,17 +40,24 @@ const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Compon
...extraParams,
});
};
handleOrderBy = (orderField, orderDir) => {
this.setState({ orderField, orderDir });
this.refreshList({ orderBy: { [orderField]: orderDir } });
};
orderByColumn = (columnName) => () =>
this.handleOrderBy(columnName, determineOrderDir(columnName, this.state.orderField, this.state.orderDir));
renderOrderIcon = (field) => {
if (this.state.orderField !== field) {
return null;
}
if (!this.state.orderDir) {
return null;
}
return (
<FontAwesomeIcon
icon={this.state.orderDir === 'ASC' ? caretUpIcon : caretDownIcon}
@@ -72,8 +80,9 @@ const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Compon
componentDidMount() {
const { match: { params }, location, shortUrlsListParams } = this.props;
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
const tags = query.tag ? [ query.tag ] : shortUrlsListParams.tags;
this.refreshList({ page: params.page, tags: query.tag ? [ query.tag ] : shortUrlsListParams.tags });
this.refreshList({ page: params.page, tags });
}
componentWillUnmount() {
@@ -103,9 +112,9 @@ const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Compon
return shortUrlsList.map((shortUrl) => (
<ShortUrlsRow
key={shortUrl.shortUrl}
shortUrl={shortUrl}
selectedServer={selectedServer}
key={shortUrl.shortCode}
refreshList={this.refreshList}
shortUrlsListParams={shortUrlsListParams}
/>
@@ -152,7 +161,7 @@ const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Compon
className="short-urls-list__header-cell short-urls-list__header-cell--with-action"
onClick={this.orderByColumn('visits')}
>
<span className="nowrap">{this.renderOrderIcon('visits')} Visits</span>
<span className="indivisible">{this.renderOrderIcon('visits')} Visits</span>
</th>
<th className="short-urls-list__header-cell">&nbsp;</th>
</tr>

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
import './UseExistingIfFoundInfoIcon.scss';
import { useToggle } from '../utils/helpers/hooks';
const renderInfoModal = (isOpen, toggle) => (
<Modal isOpen={isOpen} toggle={toggle} centered size="lg">
<ModalHeader toggle={toggle}>Info</ModalHeader>
<ModalBody>
<p>
When the&nbsp;
<b><i>&quot;Use existing URL if found&quot;</i></b>
&nbsp;checkbox is checked, the server will return an existing short URL if it matches provided params.
</p>
<p>
These are the checks performed by Shlink in order to determine if an existing short URL should be returned:
</p>
<ul>
<li>
When only the long URL is provided: The most recent match will be returned, or a new short URL will be created
if none is found.
</li>
<li>
When long URL and custom slug and/or domain are provided: Same as in previous case, but it will try to match
the short URL using both the long URL and the slug, the long URL and the domain, or the three of them.
<br />
If the slug is being used by another long URL, an error will be returned.
</li>
<li>
When other params are provided: Same as in previous cases, but it will try to match existing short URLs with
all provided data. If any of them does not match, a new short URL will be created
</li>
</ul>
</ModalBody>
</Modal>
);
const UseExistingIfFoundInfoIcon = () => {
const [ isModalOpen, toggleModal ] = useToggle();
return (
<React.Fragment>
<span title="What does this mean?">
<FontAwesomeIcon icon={infoIcon} style={{ cursor: 'pointer' }} onClick={toggleModal} />
</span>
{renderInfoModal(isModalOpen, toggleModal)}
</React.Fragment>
);
};
export default UseExistingIfFoundInfoIcon;

View File

@@ -0,0 +1,7 @@
.use-existing-if-found-info-icon__modal-quote {
margin-bottom: 0;
padding: 10px 15px;
font-size: 17.5px;
border-left: 5px solid #eee;
background-color: #f9f9f9;
}

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