Compare commits

...

516 Commits

Author SHA1 Message Date
Abhijith Vijayan
d3cd8c77e4 Merge pull request #144 from abhijithvijayan/master
Some checks failed
Build and Deploy / build (push) Has been cancelled
Build and Deploy / deploy (push) Has been cancelled
Use `browsingActivity` as a required `data_collection_permissions`
2026-02-03 22:43:02 +05:30
abhijithvijayan
3ba64a4892 v4.4.2 2026-02-03 22:41:50 +05:30
abhijithvijayan
90056d24a9 fix: use websiteActivity as we are tracking the current page url 2026-02-03 22:41:32 +05:30
Abhijith Vijayan
5dcb69b622 Merge pull request #143 from abhijithvijayan/master
[FIX] firefox build
2026-02-03 22:08:22 +05:30
abhijithvijayan
04283ad8ab Merge branch 'master' of https://github.com/abhijithvijayan/kutt-extension 2026-02-03 22:07:15 +05:30
abhijithvijayan
ac596dfef6 fix: manifest 2026-02-03 22:04:00 +05:30
abhijithvijayan
33c97b7ff8 Merge branch 'master' of https://github.com/abhijithvijayan/kutt-extension 2026-02-03 21:39:52 +05:30
Abhijith Vijayan
b9d6acdc6e Merge pull request #142 from abhijithvijayan/new-release
[FIX] Firefox store release issue
2026-02-03 21:39:31 +05:30
abhijithvijayan
ce0b8bb4b9 v4.4.1 2026-02-03 21:35:43 +05:30
abhijithvijayan
3c34baed9c fix: firefox publish issue 2026-02-03 21:35:30 +05:30
Abhijith Vijayan
6049e17105 Merge pull request #141 from abhijithvijayan/master
[CHORE] Upgrade packages
2026-01-04 18:45:36 +05:30
Abhijith Vijayan
a13d6b56ed Merge branch 'thedevs-network:master' into master 2026-01-04 18:42:34 +05:30
abhijithvijayan
d95c134ec4 fix: conflicts 2026-01-04 18:42:08 +05:30
abhijithvijayan
a01405b883 Merge branch 'master' of https://github.com/abhijithvijayan/kutt-extension 2026-01-04 18:39:36 +05:30
abhijithvijayan
6f450eb79f chore: bump packages 2026-01-04 18:37:31 +05:30
Abhijith Vijayan
56e08d1558 Merge pull request #140 from abhijithvijayan/master
[REFACTOR] Minor enhancements
2026-01-04 04:34:48 +05:30
Abhijith Vijayan
e4e9b8c744 Merge branch 'thedevs-network:master' into master 2026-01-04 04:34:08 +05:30
Abhijith Vijayan
640196b587 Merge pull request #139 from thedevs-network/dependabot/npm_and_yarn/ansi-regex-5.0.1
chore(deps-dev): bump ansi-regex from 5.0.0 to 5.0.1
2026-01-04 04:33:24 +05:30
abhijithvijayan
153bdca706 refactor: remove opera from build 2026-01-04 04:32:56 +05:30
abhijithvijayan
8404361c6a docs: update readme 2026-01-04 04:27:01 +05:30
dependabot[bot]
0ee49cabe6 chore(deps-dev): bump ansi-regex from 5.0.0 to 5.0.1
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-version: 5.0.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-03 22:42:58 +00:00
Abhijith Vijayan
cd0f6833f5 Merge pull request #138 from abhijithvijayan/master
[FEAT] Migration - Manifest v3 + Vite.js
2026-01-04 04:11:51 +05:30
abhijithvijayan
d0673e2c0a fix: minor issues 2026-01-04 04:03:25 +05:30
abhijithvijayan
0af9d929d5 fix(#117): possible fix for multi url creation 2026-01-04 03:55:16 +05:30
abhijithvijayan
85586530a8 fix(#119): custom domain issue with dropdown 2026-01-04 03:47:35 +05:30
abhijithvijayan
41e893b337 fix: linting issues 2026-01-04 03:24:00 +05:30
abhijithvijayan
444ca0a97c fix: overflowing tooltip on firefox 2026-01-04 03:16:33 +05:30
abhijithvijayan
6d01680a4b fix: history page not opening in firefox 2026-01-04 03:04:46 +05:30
abhijithvijayan
02e83642e4 v4.4.0 2026-01-04 02:53:58 +05:30
abhijithvijayan
aa9532c261 fix: session cookie stored by the website sent in the extension requests bypassing the token 2026-01-04 02:51:16 +05:30
abhijithvijayan
c121fc84d8 refactor: remove deprecated migration code 2026-01-04 02:42:35 +05:30
abhijithvijayan
cfcc9edcb4 v4.4.0-beta.1 2026-01-04 02:36:02 +05:30
abhijithvijayan
64d6c61bce fix: reset form after successful creation 2026-01-04 02:34:36 +05:30
abhijithvijayan
e0fe70ae24 refactor: enable history page by default 2026-01-04 02:30:09 +05:30
abhijithvijayan
02cf89c308 feat: clear settings menu in options page 2026-01-04 02:24:23 +05:30
abhijithvijayan
c8e6dc3a35 refactor: enhancement to dropdown in popup page 2026-01-04 02:17:10 +05:30
abhijithvijayan
4c0b31224a refactor: inline tooltip for the icons in popup page 2026-01-04 01:57:53 +05:30
abhijithvijayan
24c21f6cb8 refactor: show api key status 2026-01-04 01:53:01 +05:30
abhijithvijayan
4b15fde168 refactor: use the custom host for the header links 2026-01-04 01:40:45 +05:30
abhijithvijayan
a5bdd3a242 refactor: history page enhancements 2026-01-04 01:23:41 +05:30
abhijithvijayan
2a5f54573e refactor: enhancements in options page 2026-01-04 01:15:22 +05:30
abhijithvijayan
30dfe3b80e refactor: star effect on hover 2026-01-04 00:47:13 +05:30
abhijithvijayan
175eab6464 feat: allow reusing urls through configurable options 2026-01-04 00:41:14 +05:30
abhijithvijayan
7e25514176 fix: creating short url issue with domain 2026-01-04 00:18:22 +05:30
abhijithvijayan
e6857a68e4 refactor: use vx.x.x tag to deploy 2026-01-03 23:58:26 +05:30
abhijithvijayan
5e2df9fd26 refactor: restore comments 2026-01-03 23:50:16 +05:30
abhijithvijayan
a962d6b2b3 feat: migrate to vite and manifest v3 2026-01-03 23:17:21 +05:30
Abhijith Vijayan [FLUXON]
021db2b5fc refactor: pull new template 2026-01-03 22:16:31 +05:30
Abhijith Vijayan
8334886931 Update FUNDING.yml 2022-06-15 20:35:34 +05:30
abhijithvijayan
c7fc510bb8 chore: linting 2021-07-17 19:17:46 +05:30
abhijithvijayan
4dd0ac786c chore: bump packages 2021-07-17 19:14:52 +05:30
abhijithvijayan
24471a5a8f refactor: remove unwanted dependency 2021-07-17 19:04:26 +05:30
abhijithvijayan
ed0191f021 4.3.0-beta.2 2021-06-24 18:05:33 +05:30
abhijithvijayan
16e65b882e fix: styling issues with table in history page 2021-06-24 18:02:58 +05:30
abhijithvijayan
6e689f274c fix: disable inputs and select menu when submission is in progress 2021-06-24 17:05:38 +05:30
abhijithvijayan
cab383a081 fix: allow spaces in and as passwords 2021-06-24 16:48:49 +05:30
abhijithvijayan
e9c73947ed chore: minor 2021-06-24 16:38:00 +05:30
abhijithvijayan
efb9eaca69 fix: change issue reporting URL 2021-06-24 16:24:23 +05:30
abhijithvijayan
b2d9c2c91b fix: copy webpack plugin configuration 2021-06-24 16:08:41 +05:30
abhijithvijayan
bd9aafba6c fix: downgrade filemanager-webpack-plugin to v3.1.1 2021-06-24 16:07:52 +05:30
abhijithvijayan
96192eaa1b chore: minor 2021-06-24 15:40:52 +05:30
abhijithvijayan
7e29f373c9 chore: bump packages 2021-06-24 15:34:51 +05:30
abhijithvijayan
65b9c285cc refactor: changes to webpack config 2021-06-24 15:34:20 +05:30
abhijithvijayan
991349a8b6 refactor: migrate postcss to v8 2021-06-24 14:55:53 +05:30
abhijithvijayan
a7262f6508 refactor: minor changes to tsconfig.json 2021-06-24 14:53:33 +05:30
abhijithvijayan
a3b1e4710b refactor: use filemanager-webpack-plugin instead of zip-webpack-plugin 2021-06-24 14:52:57 +05:30
abhijithvijayan
12b4875016 fix: options page styling issues for MacOS chrome and firefox 2021-06-24 14:08:00 +05:30
abhijithvijayan
d00a43b07d refactor: use EMPTY_STRING variable 2021-06-24 03:00:51 +05:30
abhijithvijayan
7da940150f chore: linting 2021-06-24 02:48:48 +05:30
abhijithvijayan
a8b3b53947 refactor: use util functions from package 2021-06-24 02:44:40 +05:30
abhijithvijayan
f94479e9a9 fix: support IP URL shortening 2021-06-24 01:57:30 +05:30
abhijithvijayan
74b7efbb58 refactor: move files around 2021-06-24 01:12:39 +05:30
abhijithvijayan
6fbee72ae6 feat: add utilities package 2021-06-23 23:34:35 +05:30
Abhijith Vijayan
d01b1a7302 Merge pull request #106 from thedevs-network/dependabot/npm_and_yarn/lodash-4.17.21 2021-06-23 21:32:59 +05:30
Abhijith Vijayan
e6e256ea86 Merge pull request #109 from thedevs-network/dependabot/npm_and_yarn/y18n-4.0.1 2021-06-23 21:32:38 +05:30
dependabot[bot]
6ed973232f chore(deps): bump lodash from 4.17.15 to 4.17.21
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-23 16:02:36 +00:00
Abhijith Vijayan
d556037e9a Merge pull request #114 from thedevs-network/dependabot/npm_and_yarn/color-string-1.5.5 2021-06-23 21:32:19 +05:30
Abhijith Vijayan
dbe07cfd26 Merge pull request #115 from thedevs-network/dependabot/npm_and_yarn/ssri-6.0.2 2021-06-23 21:31:51 +05:30
Abhijith Vijayan
88a0aeba21 Merge pull request #111 from thedevs-network/dependabot/npm_and_yarn/hosted-git-info-2.8.9 2021-06-23 21:31:28 +05:30
dependabot[bot]
fd69442db6 chore(deps): bump ssri from 6.0.1 to 6.0.2
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

---
updated-dependencies:
- dependency-name: ssri
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-23 16:01:27 +00:00
dependabot[bot]
1d00fbf40f chore(deps): bump color-string from 1.5.3 to 1.5.5
Bumps [color-string](https://github.com/Qix-/color-string) from 1.5.3 to 1.5.5.
- [Release notes](https://github.com/Qix-/color-string/releases)
- [Changelog](https://github.com/Qix-/color-string/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Qix-/color-string/commits/1.5.5)

---
updated-dependencies:
- dependency-name: color-string
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-23 16:01:22 +00:00
Abhijith Vijayan
3850fc98ae Merge pull request #112 from thedevs-network/dependabot/npm_and_yarn/browserslist-4.16.6 2021-06-23 21:31:10 +05:30
Abhijith Vijayan
20f92b9ffd Merge pull request #113 from thedevs-network/dependabot/npm_and_yarn/ws-7.4.6 2021-06-23 21:30:35 +05:30
dependabot[bot]
407f331dfc chore(deps): bump ws from 7.2.3 to 7.4.6
Bumps [ws](https://github.com/websockets/ws) from 7.2.3 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.2.3...7.4.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-29 16:10:15 +00:00
dependabot[bot]
c11130b9cf chore(deps): bump browserslist from 4.11.0 to 4.16.6
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.11.0 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.11.0...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-25 10:18:32 +00:00
dependabot[bot]
b39518d55e chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-10 21:48:09 +00:00
abhijithvijayan
499b474040 feat: add some common type-guards utils
Signed-off-by: abhijithvijayan <34790378+abhijithvijayan@users.noreply.github.com>
2021-04-04 12:49:46 +05:30
abhijithvijayan
e2d4bf3389 fix: set eslint no-shadow rule with defaults
Signed-off-by: abhijithvijayan <34790378+abhijithvijayan@users.noreply.github.com>
2021-04-04 12:44:50 +05:30
abhijithvijayan
84f4af61f0 fix: clear timeout within useEffect hook
Signed-off-by: abhijithvijayan <34790378+abhijithvijayan@users.noreply.github.com>
2021-04-04 12:41:54 +05:30
abhijithvijayan
8857890f1b chore: add @types/node package
Signed-off-by: abhijithvijayan <34790378+abhijithvijayan@users.noreply.github.com>
2021-04-04 12:35:43 +05:30
abhijithvijayan
89edc3ec8c refactor: minor
Signed-off-by: abhijithvijayan <34790378+abhijithvijayan@users.noreply.github.com>
2021-04-04 12:35:26 +05:30
abhijithvijayan
a4801caed6 fix: bump eslint packages properly
Signed-off-by: abhijithvijayan <34790378+abhijithvijayan@users.noreply.github.com>
2021-04-04 12:08:05 +05:30
abhijithvijayan
f014c4e8a9 fix: lint only staged files
Signed-off-by: abhijithvijayan <34790378+abhijithvijayan@users.noreply.github.com>
2021-04-04 12:07:42 +05:30
abhijithvijayan
262327d71c chore: bump packages
Signed-off-by: abhijithvijayan <34790378+abhijithvijayan@users.noreply.github.com>
2021-04-04 11:55:13 +05:30
abhijithvijayan
dc6259e2e0 chore: gitignore IDE files 2021-04-04 11:54:45 +05:30
dependabot[bot]
f80b92b3b9 chore(deps): bump y18n from 4.0.0 to 4.0.1
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-30 20:02:07 +00:00
Abhijith Vijayan
1c9cee5e51 Merge pull request #105 from thedevs-network/dependabot/npm_and_yarn/elliptic-6.5.4
chore(deps): bump elliptic from 6.5.2 to 6.5.4
2021-03-09 16:30:55 +05:30
dependabot[bot]
8eb174b2c2 chore(deps): bump elliptic from 6.5.2 to 6.5.4
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 10:51:33 +00:00
abhijithvijayan
c8899aabce 4.3.0-beta.1 2020-07-21 23:36:44 +05:30
abhijithvijayan
316ac1236b feat: add history icon to popup header 2020-07-21 23:34:07 +05:30
abhijithvijayan
24b0abca77 feat: copy to clipboard in history table 2020-07-21 23:04:15 +05:30
abhijithvijayan
362b5ee457 feat: show qr code modal 2020-07-21 22:31:07 +05:30
abhijithvijayan
1fe218e969 fix: call api if and only history page option is enabled via settings 2020-07-21 22:06:09 +05:30
abhijithvijayan
9960668161 feat: use context to store and display urls history 2020-07-21 21:57:43 +05:30
abhijithvijayan
be6b078ba5 feat: add api call for fetching urls from background 2020-07-21 21:57:18 +05:30
abhijithvijayan
a28cfdcdb7 chore: add a modal for qrcode in history page 2020-07-21 21:43:22 +05:30
abhijithvijayan
a40bbb561d feat: add sample history layout 2020-07-21 03:36:26 +05:30
abhijithvijayan
9f446e6832 4.2.1 2020-07-20 23:00:07 +05:30
abhijithvijayan
7657a48961 fix: persisting spinner issue on invalid target urls 2020-07-20 22:58:52 +05:30
abhijithvijayan
c935ad2521 fix: validation issue when hydrated apikey is not of correct size in options page 2020-07-20 22:34:30 +05:30
abhijithvijayan
684d0543fd fix: type assertion for settings object keys 2020-07-20 22:25:10 +05:30
abhijithvijayan
529f22c18f 4.2.0 2020-07-14 03:03:52 +05:30
abhijithvijayan
0910b058fb fix: remove workaround check for upstream #287 2020-07-14 03:03:13 +05:30
abhijithvijayan
4a89b4a589 refactor: show only spinner on submitting 2020-07-14 03:02:39 +05:30
abhijithvijayan
582081bf12 refactor: use only local state to control spinner for create url action 2020-07-14 02:53:54 +05:30
abhijithvijayan
1d808e3003 refactor: resize all the icons 2020-07-14 02:24:52 +05:30
abhijithvijayan
1f49d929d0 fix #94: issue with missing protocol in clipboard 2020-07-14 01:56:01 +05:30
abhijithvijayan
f24004afb5 chore: meta 2020-07-14 01:53:16 +05:30
Abhijith Vijayan
36f59728be docs: update readme 2020-07-11 13:53:44 +05:30
abhijithvijayan
e6a5b9d0ee 4.1.3 2020-07-10 13:38:55 +05:30
abhijithvijayan
e6ef7f1360 fix #92: chrome popup scaling, set html height to auto 2020-07-10 13:38:38 +05:30
abhijithvijayan
a95e0f2fb3 4.1.2 2020-07-10 13:21:32 +05:30
abhijithvijayan
74a11258e0 fix #92: don't set min-height to 100vh for popup 2020-07-10 13:21:01 +05:30
abhijithvijayan
5f162bc968 4.1.1 2020-07-10 12:40:16 +05:30
abhijithvijayan
ca7387f065 fix: change to cursor pointer for popup inputs 2020-07-10 12:38:06 +05:30
abhijithvijayan
82f24de613 4.1.0 2020-07-09 22:32:47 +05:30
abhijithvijayan
5d80fd3e69 docs: add new screenshots 2020-07-09 22:32:30 +05:30
abhijithvijayan
ec02b12b27 fix: invalidate form if api key is empty in options page 2020-07-09 22:26:46 +05:30
abhijithvijayan
af0bad2716 fix: Tweaks around icons 2020-07-09 22:20:05 +05:30
abhijithvijayan
18ebdad1bc fix #91: use absolute positioned label for the input click delegation 2020-07-09 20:27:45 +05:30
abhijithvijayan
2f2d656f14 revert: use master branch as the default branch 2020-07-08 23:20:20 +05:30
abhijithvijayan
85d7f21440 4.0.0 2020-07-08 23:04:46 +05:30
abhijithvijayan
7c14f2980b fix: #85 attach domain field only if it is different from host domain 2020-07-08 23:03:46 +05:30
abhijithvijayan
a0e45392ed refactor: rename branch to main 2020-07-08 22:36:07 +05:30
abhijithvijayan
93655f5638 fix: update travis repo url 2020-07-08 22:33:37 +05:30
abhijithvijayan
68d4536a56 fix: lint error 2020-07-08 22:16:43 +05:30
abhijithvijayan
9db6f04a67 refactor: restore pre-commit hook 2020-07-08 22:14:37 +05:30
abhijithvijayan
1885f7d188 chore: bump packages 2020-07-08 22:13:31 +05:30
abhijithvijayan
958b3321dc 4.0.0-beta.8 2020-07-08 22:10:45 +05:30
abhijithvijayan
b5493ae04b chore: update meta data 2020-07-08 22:08:32 +05:30
abhijithvijayan
2180cbabcb fix: disable user-select property for most of the components 2020-07-08 22:02:14 +05:30
abhijithvijayan
9f42e8e60c 4.0.0-beta.7 2020-07-08 12:59:08 +05:30
Abhijith Vijayan
f8242e10c5 Merge pull request #90 from abhijithvijayan/context
New design
2020-07-08 12:54:14 +05:30
abhijithvijayan
3b6c6c1f72 fix: remove unwanted styles 2020-07-08 12:47:17 +05:30
abhijithvijayan
09b3a0b5ec fix: allow validation request only if form is valid 2020-07-08 12:36:09 +05:30
abhijithvijayan
0150068e07 feat: add api key validity checker call 2020-07-08 12:19:22 +05:30
abhijithvijayan
c9eb7339bf feat: show browser specific store url on options page 2020-07-08 00:09:59 +05:30
abhijithvijayan
eb7738eb13 feat: add utility to detect browser name 2020-07-08 00:08:59 +05:30
abhijithvijayan
edf646a702 feat: sync settings to localstorage 2020-07-07 23:20:35 +05:30
abhijithvijayan
6ca3c42800 feat: use react form state to manage and validate inputs 2020-07-07 22:57:21 +05:30
abhijithvijayan
63636bff39 feat: hydrate extension settings on options page mount 2020-07-07 22:00:48 +05:30
abhijithvijayan
d5e73f895c feat: redesign options page 2020-07-07 16:43:41 +05:30
abhijithvijayan
386c304b9d feat: add eye icon to show or hide password 2020-07-06 22:05:19 +05:30
abhijithvijayan
9d5057fa34 refactor: delete unwanted files 2020-07-06 03:43:56 +05:30
abhijithvijayan
467aec7864 feat: add popup header 2020-07-06 02:59:49 +05:30
abhijithvijayan
470ab06c75 feat: render select options with default value 2020-07-06 02:08:29 +05:30
abhijithvijayan
d8532d2e7a feat: show response body 2020-07-06 00:12:53 +05:30
abhijithvijayan
49d69c8d16 fix: twin.macro css & styled import props 2020-07-06 00:10:40 +05:30
abhijithvijayan
5940ac9bcb fix: handle url shortening 2020-07-05 23:00:45 +05:30
abhijithvijayan
42f7e3a13a feat: use context to manage popup screen 2020-07-05 21:06:48 +05:30
abhijithvijayan
5322e83f90 feat: use form state to handle validation and submission 2020-07-05 16:07:51 +05:30
abhijithvijayan
8d8ea6a2b4 feat: create new popup form ui with tailwindcss 2020-07-05 00:14:13 +05:30
abhijithvijayan
a50acda1bb refactor: WIP 2020-07-04 22:43:10 +05:30
abhijithvijayan
9f7e56872c chore: add react-use-form-state 2020-07-04 22:32:35 +05:30
abhijithvijayan
223c5a85ac chore: use eslint rules for node as well 2020-07-04 21:33:58 +05:30
abhijithvijayan
f7fe709091 feat: use eslint v7 and config v2 2020-07-04 21:20:16 +05:30
abhijithvijayan
9aa02f9a9d chore: bump packages 2020-07-02 16:36:52 +05:30
abhijithvijayan
e35a081c10 feat: use tailwindcss with sass & styled-components 2020-07-02 15:06:47 +05:30
abhijithvijayan
30541c551b fix: typescript config issues 2020-05-04 23:42:41 +05:30
abhijithvijayan
6fb03409fc refactor: use wext-manifest-loader 2020-05-04 23:39:45 +05:30
abhijithvijayan
6f9af25676 chore: add unicorn.log to log with 🦄 2020-04-29 00:01:50 +05:30
abhijithvijayan
5ba8026698 refactor: move files to source folder 2020-04-26 20:16:25 +05:30
abhijithvijayan
9cca9e5101 fix: apply linting 2020-04-26 20:13:48 +05:30
abhijithvijayan
6950fad830 refactor: use shared eslint config 2020-04-26 20:12:44 +05:30
abhijithvijayan
e187b8c75b refactor: (WIP) style options page 2020-04-26 20:03:57 +05:30
abhijithvijayan
20bc8ba30a fix: remove yarn release file 2020-04-26 20:02:53 +05:30
abhijithvijayan
744dbc1819 refactor: use shared ts config 2020-04-12 20:14:56 +05:30
abhijithvijayan
6c0837fe6c chore: use advanced-css-reset 2020-04-04 11:00:49 +05:30
abhijithvijayan
100a3efa53 chore: use webext-base-css for options page basic styling 2020-03-30 02:22:59 +05:30
abhijithvijayan
9f2dc342ef feat: use optimize-css-assets-webpack-plugin for css assets 2020-03-30 02:21:37 +05:30
abhijithvijayan
963485951d chore: add title property to icons on popup header 2020-03-17 11:28:13 +05:30
abhijithvijayan
d5b734665b docs: update readme 2020-03-16 21:33:08 +05:30
abhijithvijayan
2f47d84d44 4.0.0-beta.6 2020-03-16 21:30:25 +05:30
abhijithvijayan
ca7787327a refactor: style popup menu 2020-03-16 21:29:08 +05:30
abhijithvijayan
4e6213eb07 refactor: use same icons as in kutt 2020-03-16 19:55:02 +05:30
abhijithvijayan
fd4eb5271f 4.0.0-beta.5 2020-03-16 18:33:03 +05:30
abhijithvijayan
a2b9bc10eb fix: remove protocol before showing on popup screen 2020-03-16 18:32:30 +05:30
abhijithvijayan
1077348ee5 chore: bump packages 2020-03-16 17:58:18 +05:30
Abhijith Vijayan
218d63337f Merge pull request #86 from abhijithvijayan/dependabot/npm_and_yarn/acorn-6.4.1
chore(deps): [security] bump acorn from 6.4.0 to 6.4.1
2020-03-14 08:10:11 +05:30
dependabot-preview[bot]
f3ce80c20e chore(deps): [security] bump acorn from 6.4.0 to 6.4.1
Bumps [acorn](https://github.com/acornjs/acorn) from 6.4.0 to 6.4.1. **This update includes a security fix.**
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.4.0...6.4.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-03-13 20:38:24 +00:00
abhijithvijayan
612ed6c4a0 4.0.0-beta.4 2020-03-10 13:57:05 +05:30
abhijithvijayan
1a3c2ab3b9 fix #77: show error regarding v2 api if api endpoint is not found 2020-03-10 13:54:59 +05:30
abhijithvijayan
ae372d8089 fix: trim apikey & customhost field values on fetching from storage 2020-03-10 01:31:21 +05:30
abhijithvijayan
aac2b681aa 4.0.0-beta.3 2020-03-10 01:15:00 +05:30
abhijithvijayan
8a6d5ff056 feat #78: perform migration if old keys exist 2020-03-10 01:08:32 +05:30
abhijithvijayan
9f9ab82f08 fix: transform custom host url to lowercase for options form default value 2020-03-09 23:39:15 +05:30
abhijithvijayan
a03ac4995c chore: minor updates 2020-03-09 23:25:08 +05:30
abhijithvijayan
c68f6c441e feat #84: use babel + typescript = drop ts-loader 2020-03-09 23:12:04 +05:30
abhijithvijayan
94d61a6ca7 chore: add @babel/preset-typescript 2020-03-09 21:21:05 +05:30
abhijithvijayan
b2c4134610 chore: bump packages 2020-03-09 20:51:25 +05:30
abhijithvijayan
5559504d21 fix: replace x.x.x-beta.x with x.x.x.x for manifest generation 2020-03-09 20:43:06 +05:30
abhijithvijayan
20f1b0ff94 4.0.0-beta.2 2020-03-09 20:34:26 +05:30
abhijithvijayan
49172708ec refactor #83: migrate to fork-ts-checker-webpack-plugin with ts-loader 2020-03-09 20:14:52 +05:30
abhijithvijayan
5eeec4047c fix: stop terser from extracting and outputting comments 2020-03-09 19:12:51 +05:30
abhijithvijayan
8b16025f9e fix #83: travis bundle error 2020-03-09 19:12:30 +05:30
abhijithvijayan
4527884406 chore: add terser plugin & zip plugin to build compressed files 2020-03-09 00:23:58 +05:30
abhijithvijayan
8eacee8f31 refactor: use node.js 10 and later 2020-03-09 00:01:35 +05:30
abhijithvijayan
deaf0fa0cb fix: travis build 2020-03-08 23:55:31 +05:30
abhijithvijayan
b35628731c 4.0.0-beta.1 2020-03-08 23:52:52 +05:30
abhijithvijayan
dddda17ba8 refactor: remove autocopy feature 2020-03-08 23:06:46 +05:30
abhijithvijayan
5074caab21 fix: misnaming customhost field causing extension to break 2020-03-08 22:42:21 +05:30
abhijithvijayan
c3b6cf6450 fix: get hostUrl from form submission values itself in options page 2020-03-08 22:30:40 +05:30
abhijithvijayan
b685899e7b fix: attach hostUrl along requests to background script 2020-03-08 12:15:01 +05:30
abhijithvijayan
36e5dc213c feat: show host specific title in popup menu 2020-03-07 16:02:25 +05:30
abhijithvijayan
049f33576d feat: show user specific host options in popup menu 2020-03-07 15:47:37 +05:30
abhijithvijayan
1d60fc671e fix: show server validation errors on 400 2020-03-07 15:00:16 +05:30
abhijithvijayan
3336d5187c feat: add toggle menu for advanced options (customhost) 2020-03-06 23:40:58 +05:30
abhijithvijayan
fb40dd5591 chore: bump packages 2020-03-05 18:52:27 +05:30
Abhijith Vijayan
494286bb15 Create FUNDING.yml 2020-02-24 16:46:06 +05:30
abhijithvijayan
3baf48fc6a Merge branch 'master' of https://github.com/abhijithvijayan/kutt-extension 2020-02-13 09:16:18 +05:30
Abhijith Vijayan
be7a291301 Merge pull request #81 from ashikmeerankutty/ui
Redesign Popup UI
2020-02-13 08:52:01 +05:30
Ashik Meerankutty
33aec2d574 ui : Redesigned Popup UI 2020-02-13 08:08:19 +05:30
abhijithvijayan
1173a80423 feat: add sample history page 2020-02-12 14:02:23 +05:30
abhijithvijayan
f4a40bf9b7 fix: reuse parent level spinner on form submission 2020-02-11 15:40:32 +05:30
abhijithvijayan
b07a1f0226 refactor: move popup body along the form 2020-02-11 14:46:32 +05:30
abhijithvijayan
11c9915045 feat: add qrcode functionality on Popup menu 2020-02-11 12:00:53 +05:30
abhijithvijayan
d4d5db467e fix: version to match with v1 branch 2020-02-11 06:48:28 +05:30
abhijithvijayan
052356a82e feat: add copying functionality for popup page 2020-02-11 06:44:21 +05:30
abhijithvijayan
903a20d6af chore: open options page if no api key is set 2020-02-10 21:32:42 +05:30
abhijithvijayan
3dd2239daa feat: reload popup form on receiving refresh button response 2020-02-10 20:57:11 +05:30
abhijithvijayan
8c1991b930 refactor: spin& show status on refresh button action 2020-02-10 14:30:13 +05:30
abhijithvijayan
fa58dcbaaa feat: add a refresh button to fetch domains to header 2020-02-10 11:54:30 +05:30
abhijithvijayan
0ee5d08f01 feat: show apikey validation response in options page 2020-02-10 03:53:36 +05:30
abhijithvijayan
d4d21adc44 chore: add cross & tick icons 2020-02-10 03:45:52 +05:30
abhijithvijayan
39c89b2028 fix: type errors in options form 2020-02-10 02:16:03 +05:30
abhijithvijayan
9cf969ff50 refactor: replace go back text with arrow icon 2020-02-09 22:27:30 +05:30
abhijithvijayan
4d00612d0e chore: add refresh & arrowleft icons 2020-02-09 22:25:27 +05:30
abhijithvijayan
be7d498123 fix: build failing issue 2020-02-09 22:18:00 +05:30
Abhijith Vijayan
24e206bb2b Merge pull request #80 from abhijithvijayan/refactor/react-typescript
TypeScript + React Complete Rewrite
2020-02-09 21:58:15 +05:30
abhijithvijayan
aa7b199969 feat: get apikey on popup mount & attach to shorten request 2020-02-09 21:31:21 +05:30
abhijithvijayan
66ad7e5dc2 fix: (#79) enable setSubmitting flag before response dom update 2020-02-09 10:03:59 +05:30
abhijithvijayan
2c801b9cde feat: show error if no api key set & use defaults if user field not set 2020-02-09 10:02:33 +05:30
abhijithvijayan
4e7bcbf13c feat: show invalid url error 2020-02-08 22:24:45 +05:30
abhijithvijayan
80f4469beb feat: add go back button on response screen 2020-02-08 21:39:13 +05:30
abhijithvijayan
92cea5b5f4 feat: show API responses on UI 2020-02-08 21:29:43 +05:30
abhijithvijayan
d7db49dbc5 fix: type errors on popup form 2020-02-08 18:34:54 +05:30
abhijithvijayan
2888a7f262 fix: minor tweaks to namings 2020-02-08 17:35:24 +05:30
abhijithvijayan
942a1cb62c feat: fill options page with preferences from storage 2020-02-08 17:33:42 +05:30
abhijithvijayan
5ae1af75e6 feat: get target link from querying browser tabs 2020-02-08 16:08:22 +05:30
abhijithvijayan
fd43dc7a5f feat: show spinner on shortening 2020-02-08 12:47:23 +05:30
abhijithvijayan
426141223f feat: add a loading spinner 2020-02-08 12:46:58 +05:30
abhijithvijayan
6ebc47d79c feat: basic styling across all components 2020-02-08 11:53:53 +05:30
abhijithvijayan
ce16e897f6 feat: get domains list from storage & load on dropdown menu in popup screen 2020-02-08 10:53:50 +05:30
abhijithvijayan
844d60663d fix: delete saved user data if api key is invalid 2020-02-07 21:52:31 +05:30
abhijithvijayan
42f139d640 feat: save user information to storage on successful api validation 2020-02-07 15:25:50 +05:30
abhijithvijayan
cb2fb2784b refactor: add typings for options actions 2020-02-07 14:00:05 +05:30
abhijithvijayan
e5b5042fd5 refactor: add typings to popup create action 2020-02-07 13:30:25 +05:30
abhijithvijayan
3f8c1e1048 fix: handle shortening submission using try-catch in background-scripts 2020-02-07 10:40:35 +05:30
abhijithvijayan
87b1522258 fix: validate & add customurl & password fields as optional 2020-02-07 10:31:33 +05:30
abhijithvijayan
6406095b6c fix: api key validation request 2020-02-07 10:31:03 +05:30
abhijithvijayan
1d2bd4c973 refactor: move options page helpers around 2020-02-06 20:49:58 +05:30
abhijithvijayan
d9666aaf86 refactor: move files around 2020-02-06 20:45:24 +05:30
abhijithvijayan
4bfc6d5eab feat: save setting to local storage on form value change 2020-02-06 06:28:02 +05:30
abhijithvijayan
4e31d16813 feat: add custom auto save util for form 2020-02-06 06:00:12 +05:30
abhijithvijayan
bf3c249f12 chore: add checkbox components to options page 2020-02-06 03:55:52 +05:30
abhijithvijayan
9c61300f66 feat: request v2 links api to shorten on popup button click 2020-02-04 11:46:07 +05:30
abhijithvijayan
4d4fbfcdf7 feat: send request to api on api key validate action 2020-02-04 10:32:05 +05:30
abhijithvijayan
5d25d31e82 chore: add axios package 2020-02-04 10:29:57 +05:30
abhijithvijayan
695bf7e00f feat: add options page form 2020-02-02 11:26:32 +05:30
abhijithvijayan
f61f1ae0d9 docs: update README 2020-02-02 10:43:31 +05:30
abhijithvijayan
187d6ce00f feat: use edge v79 (chromium edge) for babel 2020-02-02 10:43:21 +05:30
abhijithvijayan
dec5a92a03 fix: remove stylesheet entries from source html 2020-01-31 15:50:52 +05:30
abhijithvijayan
f8cde482f4 fix: styling issues for components 2020-01-31 15:50:24 +05:30
abhijithvijayan
0c924341cb refactor: remove webpack-fix-style-only-entries packages 2020-01-31 15:44:13 +05:30
abhijithvijayan
c32ac0d7c5 refactor: remove style only entries 2020-01-31 15:42:30 +05:30
abhijithvijayan
a33a5b0891 fix: use MiniCssExtractPlugin in webpack config for sass/scss/css 2020-01-31 15:41:53 +05:30
abhijithvijayan
45767fd574 fix: use src/ as source for typescript 2020-01-31 15:37:23 +05:30
abhijithvijayan
885336dd1c refactor: use mini-css-extract-plugin instead extract-loader 2020-01-31 15:36:18 +05:30
abhijithvijayan
4b0ba22542 chore: open options page on settings button click 2020-01-30 16:50:30 +05:30
abhijithvijayan
f8056a1046 fix: replace JSX.Element with React.FC 2020-01-30 15:13:18 +05:30
abhijithvijayan
db186ceeb1 feat: add header section with logo 2020-01-30 15:07:35 +05:30
abhijithvijayan
e14e36279b fix: use formik typings for popup components 2020-01-30 13:44:14 +05:30
abhijithvijayan
8893f10132 chore: show form on popup menu loading 2020-01-30 12:25:20 +05:30
abhijithvijayan
f8763091bc feat: create a formik popup form component 2020-01-30 12:25:02 +05:30
abhijithvijayan
1321777b01 chore: add few react/jsx-* rules 2020-01-30 12:23:04 +05:30
abhijithvijayan
9ec54c1e17 chore: add formik forms library 2020-01-30 12:22:33 +05:30
abhijithvijayan
01bad479d7 chore: add new flags for ts config 2020-01-29 20:30:03 +05:30
abhijithvijayan
b96782e388 chore: bump dependencies 2020-01-27 22:04:30 +05:30
abhijithvijayan
3cbbbfbc50 fix: use environmental plugin to pass env vars 2020-01-27 21:56:04 +05:30
abhijithvijayan
d0c485120a fix: import/resolver imports warning 2020-01-27 21:52:32 +05:30
abhijithvijayan
cbd7f25ea4 chore: set "allowSyntheticDefaultImports": true, 2020-01-27 21:50:26 +05:30
abhijithvijayan
819cae5c48 chore: add stylesheet entry to views html 2020-01-27 11:46:44 +05:30
abhijithvijayan
b0b375efec feat: use extract-text-webpack-plugin to remove style only entry generating js file 2020-01-27 11:44:07 +05:30
abhijithvijayan
fb81e110fa feat: use webpack to compile scss files to css 2020-01-27 11:38:55 +05:30
abhijithvijayan
6a6fd6c3f4 chore: add sample sass files 2020-01-27 11:27:58 +05:30
abhijithvijayan
5d5cb5f61f fix: don't lint js files 2020-01-27 11:13:56 +05:30
abhijithvijayan
bcb1ce322e chore: set allowJs to true 2020-01-27 11:12:50 +05:30
abhijithvijayan
466f896218 fix: (#74) set yarn to v1 for now 2020-01-27 10:51:36 +05:30
Abhijith Vijayan
8f290b5c90 Merge master to refactor/react-typescript (#75)
Merge master to refactor/react-typescript
2020-01-27 09:50:10 +05:30
abhijithvijayan
64f2314f65 Merge branch 'refactor/react-typescript' of https://github.com/abhijithvijayan/kutt-extension into refactor/react-typescript 2020-01-27 09:45:55 +05:30
abhijithvijayan
607032e748 feat: add packages for sass support 2020-01-27 09:44:40 +05:30
abhijithvijayan
d4632552e6 fix: eslint configuration 2020-01-26 20:51:22 +05:30
abhijithvijayan
b6ea953092 fix: eslint configuration 2020-01-26 20:46:07 +05:30
abhijithvijayan
4f6f2027c0 chore: bump packages 2020-01-26 20:44:56 +05:30
abhijithvijayan
ff7faac743 chore: resolve webextension-polyfill-ts alias 2020-01-26 20:36:27 +05:30
abhijithvijayan
927984bcdc refactor: change html files path 2020-01-26 20:30:30 +05:30
abhijithvijayan
b64f2bfbc9 feat: add webextension-polyfill-ts 2020-01-26 20:22:43 +05:30
abhijithvijayan
c02e053f51 fix: manifest background script path 2020-01-26 20:07:09 +05:30
abhijithvijayan
aeee3b3a38 chore: add awcache dir to gitignore 2020-01-26 20:06:09 +05:30
abhijithvijayan
31b2a63176 feat: use babel with awesome-typescript-loader 2020-01-26 20:05:50 +05:30
abhijithvijayan
709b41b314 chore: add babel configuration file 2020-01-26 20:04:39 +05:30
abhijithvijayan
8bf257a01c feat: add babel/core & plugins with runtime 2020-01-26 20:04:14 +05:30
Abhijith Vijayan
f63454be47 Update LICENSE 2020-01-24 06:59:52 +05:30
Abhijith Vijayan
50d530128f work in progress 2020-01-20 19:48:42 +05:30
abhijithvijayan
ca05e49d9b feat: add precommit lint hook 2020-01-17 05:15:14 +05:30
abhijithvijayan
0ef0a44b2e chore: apply linting 2020-01-17 05:15:01 +05:30
abhijithvijayan
07af3d0f12 feat: add eslint-config-onepass with ts 2020-01-17 05:08:47 +05:30
abhijithvijayan
f301127035 refactor: restore assets, manifest & delete old files 2020-01-17 04:57:06 +05:30
abhijithvijayan
69514a9ac1 feat: add webpack-extension-reloader to reload browser 2020-01-17 04:45:44 +05:30
abhijithvijayan
d55380ba55 feat: use wext-manifest & write-webpack-plugin to generate browser specific manifests 2020-01-17 04:33:48 +05:30
abhijithvijayan
e4a11beab5 refactor: change ts options for browser build 2020-01-17 04:32:38 +05:30
abhijithvijayan
8c362de82f feat: use clean-webpack-plugin to delete build dir 2020-01-17 04:15:02 +05:30
abhijithvijayan
c77284ae02 chore: use copy-webpack-plugin for static assets 2020-01-17 04:12:48 +05:30
abhijithvijayan
e144cabc16 feat: build static files to extension/browser dir 2020-01-17 04:08:16 +05:30
abhijithvijayan
3de169ec37 chore: add cross-env & update build & dev scripts 2020-01-17 04:06:00 +05:30
abhijithvijayan
fe195af30b feat: use html-webpack-plugin to build static html files 2020-01-17 04:03:47 +05:30
abhijithvijayan
ca954d567a chore: build js to a subdirectory 2020-01-17 03:51:30 +05:30
abhijithvijayan
101988e1d8 chore: add ts files 2020-01-17 03:33:36 +05:30
abhijithvijayan
449df5adb2 chore: fix webpack config 2020-01-17 03:33:27 +05:30
abhijithvijayan
764662f044 chore: add dist/ to gitignore 2020-01-17 03:32:58 +05:30
abhijithvijayan
b3c3d17ee4 chore: minor tweaks 2020-01-17 02:04:07 +05:30
abhijithvijayan
dee760871d chore: add react, react-dom, ts, webpack packages 2020-01-16 12:24:31 +05:30
abhijithvijayan
799cedf73e chore: add ts configuration 2020-01-16 12:19:09 +05:30
abhijithvijayan
af5b08bee8 feat: set up webpack configuration for ts 2020-01-16 12:15:28 +05:30
abhijithvijayan
0177c6beb0 refactor: reinitialize package.json 2020-01-16 06:28:06 +05:30
abhijithvijayan
6ec3ef0e96 refactor: move old source files to a folder 2020-01-16 06:25:21 +05:30
Abhijith Vijayan
4f2f1a9c67 Update README.md 2019-12-30 15:21:53 +05:30
abhijithvijayan
6c05d076c9 3.2.1 2019-12-12 20:59:57 +05:30
abhijithvijayan
b9b8550fe5 feat: change tabs permission to activeTab for security 2019-12-12 20:59:24 +05:30
abhijithvijayan
e804970a4d refactor: use concise and clear constants 2019-12-12 20:24:47 +05:30
abhijithvijayan
5172947ab4 3.2.0 2019-12-12 18:38:06 +05:30
abhijithvijayan
6f3f6f06bb refactor: move all constants to another file 2019-12-12 18:37:46 +05:30
abhijithvijayan
82fce1b475 feat: add dashboard link button to history page 2019-12-12 18:26:30 +05:30
abhijithvijayan
08e225946d use "noopener noreferrer nofollow" for _blank links 2019-12-12 17:41:37 +05:30
abhijithvijayan
b0f327ddc2 chore: do not use browser_style for options page 2019-12-12 17:40:44 +05:30
abhijithvijayan
a2428bfba1 fix: oh c'mon, use yarn 2019-12-12 07:38:35 +05:30
abhijithvijayan
ccb1931845 fix: use yarn to perform travis build 2019-12-12 07:36:49 +05:30
Abhijith Vijayan
8be5251091 chore: use node.js 12 for travis as well 2019-12-12 07:34:47 +05:30
abhijithvijayan
40f82f5dd8 3.1.2 2019-12-12 07:30:24 +05:30
abhijithvijayan
2d365b0226 chore: use node.js 12 2019-12-12 07:29:47 +05:30
abhijithvijayan
35f9be8f8a chore: bump dependencies 2019-12-12 07:29:24 +05:30
abhijithvijayan
e88366f8f1 fix: add strict_min_version for firefox to manifest 2019-11-23 00:08:31 +05:30
Abhijith Vijayan
48a57c7afc ignore all haters 2019-11-09 10:03:25 +05:30
abhijithvijayan
55a1867e7c 3.1.1 2019-11-04 20:17:39 +05:30
abhijithvijayan
41bc2e9d74 refactor: tweaks 2019-11-04 19:24:55 +05:30
abhijithvijayan
4199208690 fix: minor tweaks 2019-11-04 18:58:07 +05:30
abhijithvijayan
948e20ae81 chore: bump wext-manifes to 2.0.1 2019-11-01 19:15:44 +05:30
abhijithvijayan
0d52672dda fix: pack into corresponding browser supported file formats 2019-11-01 19:09:19 +05:30
abhijithvijayan
61ab3bf3cc refactor: move all manifest content to src/manifest/index.js 2019-11-01 19:04:26 +05:30
abhijithvijayan
f18ff1aed6 chore: set up webpack to copy vendor specfic manifest file 2019-11-01 18:54:33 +05:30
abhijithvijayan
72b4c85508 refactor: add sample manifest file 2019-11-01 18:54:06 +05:30
abhijithvijayan
d35d797afe refactor: add write-webpack-plugin 2019-11-01 18:48:50 +05:30
abhijithvijayan
8613a5c679 refactor: rename TARGET to TARGET_BROWSER 2019-11-01 18:48:27 +05:30
abhijithvijayan
ec4dd32618 refactor: add wext-manifest package 2019-11-01 18:42:35 +05:30
abhijithvijayan
e6c341336c v3.1.0 2019-10-25 20:40:42 +05:30
abhijithvijayan
794ad78927 move sass files around 2019-10-25 20:39:43 +05:30
abhijithvijayan
449caae4c8 upgrade dependencies 2019-10-25 20:38:26 +05:30
abhijithvijayan
683e269e6b append package.json version to manifest.json dynamically on build 2019-10-25 20:30:43 +05:30
abhijithvijayan
381407fa08 remove version field from manifest.json files 2019-10-25 20:28:15 +05:30
abhijithvijayan
09899a55fc move manifest files 2019-10-25 20:25:27 +05:30
abhijithvijayan
8262ec791a 3.0.2 2019-09-24 23:15:08 +05:30
abhijithvijayan
5e247c57ee update packages 2019-09-24 23:10:36 +05:30
abhijithvijayan
1b44a0ffc2 upgrade eslint config 2019-09-24 22:51:32 +05:30
Abhijith Vijayan
2d9e2f108d update firefox store reviews link 2019-08-24 18:46:21 +05:30
Abhijith Vijayan
dc6600d912 [Security] Bump lodash from 4.17.11 to 4.17.14 (#68)
[Security] Bump lodash from 4.17.11 to 4.17.14
2019-07-11 10:43:53 +05:30
Abhijith Vijayan
7863e81325 [Security] Bump lodash.template from 4.4.0 to 4.5.0 (#67)
[Security] Bump lodash.template from 4.4.0 to 4.5.0
2019-07-11 10:43:41 +05:30
dependabot-preview[bot]
14c4c020d6 [Security] Bump lodash from 4.17.11 to 4.17.14
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14. **This update includes security fixes.**
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-10 21:58:52 +00:00
dependabot-preview[bot]
8ee221e165 [Security] Bump lodash.template from 4.4.0 to 4.5.0
Bumps [lodash.template](https://github.com/lodash/lodash) from 4.4.0 to 4.5.0. **This update includes security fixes.**
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.4.0...4.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-10 20:29:06 +00:00
abhijithvijayan
3d945f8d54 fix clean-webpack-plugin deletion issue 2019-07-04 15:35:20 +05:30
abhijithvijayan
00a08f7122 remove trailing slash(/) for custom host Fix #66 2019-07-04 10:55:55 +05:30
abhijithvijayan
8716d89c7c update README.md 2019-07-03 01:18:35 +05:30
abhijithvijayan
55c4080ed2 set node-versions 2019-07-03 01:15:13 +05:30
Abhijith Vijayan
c5d2c2e652 Update README.md 2019-07-03 01:10:32 +05:30
abhijithvijayan
c7050c4021 changes to webpack.config.js 2019-07-03 00:58:37 +05:30
abhijithvijayan
f796a1f6ef bump version 2019-07-03 00:43:43 +05:30
abhijithvijayan
30f68b21f5 use eslint-config-onepass and perform linting 2019-07-03 00:37:11 +05:30
abhijithvijayan
c0980fb3fc save upto 15 shortened under history 2019-07-03 00:08:08 +05:30
abhijithvijayan
a5f7c153ad revert to axios for api calls 2019-07-03 00:06:55 +05:30
abhijithvijayan
acd2b4aaa5 update scripts 2019-07-02 22:18:46 +05:30
abhijithvijayan
e86283ebb9 switch to yarn 2019-07-02 22:15:54 +05:30
Abhijith V
32a327cbf9 Merge pull request #61 from abhijithvijayan/dependabot/npm_and_yarn/axios-0.18.1
[Security] Bump axios from 0.18.0 to 0.18.1
2019-06-04 08:45:32 +05:30
dependabot-preview[bot]
b866df2462 [Security] Bump axios from 0.18.0 to 0.18.1
Bumps [axios](https://github.com/axios/axios) from 0.18.0 to 0.18.1. **This update includes security fixes.**
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.18.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.18.0...v0.18.1)
2019-06-03 19:47:06 +00:00
abhijithvijayan
522d58956b update to manifest.json 2019-06-01 18:37:32 +05:30
abhijithvijayan
7494967b3e rate button 2019-05-24 08:47:31 +05:30
abhijithvijayan
d2a0474aff UI tweaks | history page | options page 2019-05-24 07:41:06 +05:30
Abhijith V
56ded3e018 Merge pull request #54 from abhijithvijayan/duplication-fix
Duplication fix
2019-05-17 17:38:19 +05:30
abhijithvijayan
1b8686db11 updated dependencies 2019-05-17 17:34:47 +05:30
abhijithvijayan
2f5ad60647 history: replace old shortened url with newer 2019-05-17 17:32:12 +05:30
abhijithvijayan
171b260b28 minor refactor 2019-05-17 16:23:33 +05:30
Abhijith V
3cf349649e Merge pull request #53 from abhijithvijayan/autocopy
bug fixes : copying in firefox
2019-05-17 12:34:19 +05:30
abhijithvijayan
718568be36 bug fixes 2019-05-17 12:29:54 +05:30
abhijithvijayan
70bf186823 fixed autocopy bug 2019-05-16 19:31:39 +05:30
abhijithvijayan
644ff25af1 change permission to access to other hosts too 2019-05-16 04:45:34 +05:30
Abhijith V
019dd853d5 Merge pull request #47 from abhijithvijayan/dependabot/npm_and_yarn/webpack-cli-3.3.2
Bump webpack-cli from 3.3.0 to 3.3.2
2019-05-11 08:22:42 +05:30
dependabot[bot]
1daa75851c Bump webpack-cli from 3.3.0 to 3.3.2
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 3.3.0 to 3.3.2.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/v.3.3.0...v3.3.2)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-11 02:48:10 +00:00
abhijithvijayan
f6bbf9ee29 info updates 2019-05-10 23:00:40 +05:30
Abhijith V
020608daf4 Merge pull request #46 from abhijithvijayan/feature/selfhostedkutt
selfhostedkutt : core functionality
2019-05-10 22:45:20 +05:30
abhijithvijayan
6bb9fb6830 core functionality 2019-05-10 22:36:44 +05:30
abhijithvijayan
18cbfa8097 minor changes 2019-05-10 20:41:55 +05:30
Abhijith V
80bb7c9ada Merge pull request #45 from abhijithvijayan/refactoring
Refactoring code
2019-05-10 19:50:15 +05:30
abhijithvijayan
1008ef0dca minor fixes 2019-05-10 19:42:20 +05:30
abhijithvijayan
91e265a41a refactoring history page 2019-05-10 12:11:24 +05:30
abhijithvijayan
7c1114525a refactoring options page 2019-05-10 11:28:51 +05:30
abhijithvijayan
4b061aed62 huge refactoring 2019-05-10 11:09:32 +05:30
abhijithvijayan
acf02b0eec replacing promises with async-await 2019-05-10 10:30:31 +05:30
abhijithvijayan
9ddbe7a0dc fixed vulnerabilities in npm packages 2019-05-10 09:07:45 +05:30
abhijithvijayan
ec0f1d0577 UI bug fixes 2019-05-10 08:11:35 +05:30
abhijithvijayan
a568a908cd set timeout to 20 seconds 2019-04-01 22:26:05 +05:30
abhijithvijayan
7ec562b49f screen flash/flicker issue fix 2019-03-03 11:47:40 +05:30
Abhijith V
ec2f0518ab Merge pull request #41 from abhijithvijayan/bling.js
bling.js
2019-03-03 10:45:05 +05:30
abhijithvijayan
3e851262c5 bling.js 2019-03-03 10:38:18 +05:30
abhijithvijayan
a189c6cfb3 version bump 2019-03-03 09:16:19 +05:30
abhijithvijayan
7a6d41114b bug fixes 2019-02-23 09:41:40 +05:30
Abhijith V
5dcae5dc92 Merge pull request #39 from abhijithvijayan/webpack-changes
FixStyleOnlyEntriesPlugin
2019-02-23 09:24:04 +05:30
abhijithvijayan
150c18b5f3 FixStyleOnlyEntriesPlugin 2019-02-23 08:26:31 +05:30
abhijithvijayan
76f78e77fd code styling 2019-02-21 20:10:02 +05:30
abhijithvijayan
1806e90e8a code refactoring 2019-02-17 20:15:16 +05:30
abhijithvijayan
780b2ca674 version bump 2019-02-17 13:25:48 +05:30
abhijithvijayan
64454c7e05 removed unwanted assets & minor UI changes 2019-02-17 10:13:58 +05:30
abhijithvijayan
8c4a16d71d removed unwanted actions 2019-02-16 19:17:40 +05:30
abhijithvijayan
06819b8c64 variable types 2019-02-15 08:12:50 +05:30
abhijithvijayan
643cdc49b7 UI improvements 2019-02-14 08:35:55 +05:30
abhijithvijayan
d07339470f adapting to store users current setup 2019-02-13 18:48:07 +05:30
Abhijith V
fd6de88570 Merge pull request #34 from abhijithvijayan/options
configurable options
2019-02-11 00:21:13 +05:30
abhijithvijayan
ca07fe7d6d configurable options 2019-02-11 00:16:41 +05:30
abhijithvijayan
ac5e3d4b89 history page buttons & overall fixes 2019-02-10 14:52:37 +05:30
Abhijith V
a4932364a0 Merge pull request #33 from abhijithvijayan/refactor
fixed major bugs
2019-02-10 12:35:35 +05:30
abhijithvijayan
39cd8ae64c fixed major bugs 2019-02-10 12:19:49 +05:30
abhijithvijayan
0f99aaeff6 timeout : node-kutt 2019-02-10 05:02:44 +05:30
abhijithvijayan
3a1378c7bc fixed bugs 2019-02-10 04:49:23 +05:30
Abhijith V
682908fe06 feature/history
Feature/history
2019-02-07 23:34:34 +05:30
abhijithvijayan
9f5ced28e9 history page: clear, qrcode & fixes 2019-02-07 23:30:19 +05:30
abhijithvijayan
09b27265e3 history items injection (10 entries) 2019-02-07 08:15:59 +05:30
abhijithvijayan
dc976b9bee history UI + implementation(barebone) 2019-02-06 22:13:22 +05:30
abhijithvijayan
c1faa4387d updates 2019-02-04 21:12:06 +05:30
Abhijith V
edba29be6c Merge pull request #27 from aarjae/node
Kutt package
2019-02-04 20:36:56 +05:30
rjforty
a5b4d81274 Changed API call system to rely on kutt package 2019-02-04 10:21:26 +00:00
rjforty
e9fd7640b5 Added kutt package to project 2019-02-04 10:20:23 +00:00
rjforty
b573b05d98 Changed line seperators from LF to CRLF based on current eslint config 2019-02-04 09:26:17 +00:00
abhijithvijayan
02e01d2fe2 support to old most used browser versions 2019-02-02 11:45:12 +05:30
abhijithvijayan
2b24069bc5 version bump 2019-02-02 10:41:48 +05:30
abhijithvijayan
9defdc3b8c timeout feature 2019-01-20 09:05:35 +05:30
Abhijith V
3d5cebd9fd Update README.md 2019-01-19 20:47:44 +05:30
abhijithvijayan
b5f3a0de69 chrome extension link & linting 2019-01-19 18:58:29 +05:30
abhijithvijayan
223fe002dd auto close tab on save 2019-01-16 09:39:37 +05:30
Abhijith V
dcb74735e9 Merge pull request #26 from abhijithvijayan/eslint
ESLint addition
2019-01-14 18:25:35 +05:30
abhijithvijayan
d4b6559efa ESLint 2019-01-14 18:19:50 +05:30
abhijithvijayan
06670970c1 no API auto open options page 2019-01-14 10:57:56 +05:30
abhijithvijayan
dfee32ec8c Options page tooltip 2019-01-13 21:09:16 +05:30
abhijithvijayan
5e17f4430c firefox add on link to readme 2019-01-13 19:54:11 +05:30
abhijithvijayan
ae25514d0a minor changes 2019-01-13 16:14:12 +05:30
abhijithvijayan
67fb064da4 Zip dir relocate 2019-01-13 16:04:40 +05:30
Abhijith V
91acae42cd Merge pull request #25 from abhijithvijayan/feature/compress
Feature/compressZip
2019-01-13 15:47:09 +05:30
abhijithvijayan
a5641ae411 Major changes: cross-env build dir & updates 2019-01-13 15:42:22 +05:30
abhijithvijayan
2bf9423a3f ZipPlugin 2019-01-13 14:41:40 +05:30
Abhijith V
bb827e21e7 Merge pull request #24 from abhijithvijayan/qrcode
QR Code package
2019-01-13 13:31:16 +05:30
abhijithvijayan
fe21db5928 QR Code package 2019-01-13 13:27:31 +05:30
abhijithvijayan
e4ad828b5c status codes 2019-01-13 10:53:29 +05:30
abhijithvijayan
ddc747d7e7 Fixes and Minor Changes to Styling 2019-01-13 10:09:38 +05:30
Abhijith V
acb784f744 Merge pull request #23 from poeti8/master
Renaming and style improvements
2019-01-13 08:28:09 +05:30
poeti8
b8ba3b17bc Improve overall style 2019-01-12 21:28:41 +03:30
poeti8
48271fa26f Change font to Nunito 2019-01-12 21:28:34 +03:30
poeti8
6dee1a79af Fix details link 2019-01-12 21:28:21 +03:30
poeti8
894c264bfc Icon colors 2019-01-12 21:28:05 +03:30
poeti8
64fb845db8 Remove extra icons 2019-01-12 21:27:51 +03:30
poeti8
5fe73a0f73 Rename kutt 2019-01-12 21:27:39 +03:30
poeti8
0866ccb14e Make API field's type to be password 2019-01-12 21:27:30 +03:30
poeti8
df2496f4cd Renaming kuttUrl to Kutt 2019-01-12 19:06:36 +03:30
abhijithvijayan
6146d2a9df build separation 2019-01-12 00:15:01 +05:30
abhijithvijayan
270b87d29f cleanups and fixes 2019-01-11 20:43:45 +05:30
abhijithvijayan
652c7a6682 webextension-polyfill to package.json 2019-01-11 18:04:41 +05:30
abhijithvijayan
5bb6fea424 TerserPlugin for minifying js 2019-01-11 17:50:23 +05:30
abhijithvijayan
fe510120a7 babel browser versions 2019-01-11 17:15:31 +05:30
abhijithvijayan
293f0fcd59 error code 429 2019-01-11 15:32:19 +05:30
Abhijith V
7182dcb300 Merge pull request #19 from abhijithvijayan/webextension
Webextension transformation
2019-01-11 13:21:43 +05:30
abhijithvijayan
c77fc01dd5 babel issues [Fix] 2019-01-11 13:17:15 +05:30
abhijithvijayan
7564e7ecfd Major changes: babel-runtime 2019-01-11 12:20:06 +05:30
abhijithvijayan
930dee73e6 Merge branch 'master' into webextension 2019-01-11 10:25:44 +05:30
abhijithvijayan
5cb571eba2 valid url check 2019-01-11 10:22:53 +05:30
abhijithvijayan
585340fdc2 badge addition 2019-01-10 12:21:15 +05:30
abhijithvijayan
bd905e6b3d webpack changes 2019-01-10 11:51:04 +05:30
Abhijith V
0141c30cb8 Merge pull request #18 from abhijithvijayan/webpack-polyfill
webextension-polyfill library
2019-01-10 11:21:58 +05:30
abhijithvijayan
a9c0e32381 library seperation 2019-01-10 11:18:59 +05:30
abhijithvijayan
0d445c68da added descriptive instructions 2019-01-10 09:48:32 +05:30
Abhijith V
e31676348f Merge pull request #15 from abhijithvijayan/webext
WebExtension API
2019-01-09 20:48:12 +05:30
abhijithvijayan
bc91d72c8a minor fixes 2019-01-09 20:30:29 +05:30
abhijithvijayan
06561ed6cc promise return function 2019-01-09 17:42:24 +05:30
abhijithvijayan
8b8b0662a6 sendResponse 2019-01-09 14:48:00 +05:30
abhijithvijayan
b9f9f3b120 webextension-polyfill 2019-01-09 13:09:29 +05:30
abhijithvijayan
fa5fcfc498 API CORS support 2019-01-09 01:29:39 +05:30
abhijithvijayan
b965828d51 html minifier 2019-01-08 16:22:23 +05:30
Abhijith V
3b11b3e17a Merge pull request #12 from abhijithvijayan/release
New Release
2019-01-08 01:37:05 +05:30
abhijithvijayan
3d384cb8f8 Merge branch 'master' of https://github.com/abhijithvijayan/kuttUrl-Chrome into release 2019-01-08 01:32:59 +05:30
abhijithvijayan
7b49783bad keypress function 2019-01-08 01:32:30 +05:30
Abhijith V
296453a9dc Merge pull request #11 from abhijithvijayan/sassminify
Sassminify in Production
2019-01-08 00:49:03 +05:30
abhijithvijayan
11d7416daf sourcemaps removal 2019-01-08 00:44:50 +05:30
abhijithvijayan
aae6bd8516 [Fix] JS Compression Issue: UglifyJSPlugin 2019-01-08 00:07:45 +05:30
abhijithvijayan
2a260ed450 OptimizeCssAssetsPlugin added: breaks js compression 2019-01-07 23:28:13 +05:30
abhijithvijayan
029ce681e5 reset.scss 2019-01-07 22:29:56 +05:30
abhijithvijayan
ec46c594b3 minor UI Changes 2019-01-07 20:27:32 +05:30
abhijithvijayan
10ee8eea4a Options Page UI 2019-01-07 17:37:51 +05:30
abhijithvijayan
cca3ec0362 details icon change 2019-01-07 15:32:01 +05:30
abhijithvijayan
58d2362fba badge addition 2019-01-07 14:32:08 +05:30
abhijithvijayan
ac6ca0249e minor changes 2019-01-07 11:17:55 +05:30
abhijithvijayan
fa5cf83c7d Password Feature 2019-01-07 10:55:39 +05:30
abhijithvijayan
a35788328b Options page UI 2019-01-07 08:34:48 +05:30
105 changed files with 12524 additions and 9979 deletions

View File

@@ -1,3 +0,0 @@
{
"presets": ["@babel/preset-env"]
}

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

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: [abhijithvijayan]
patreon: abhijithvijayan
open_collective: abhijithvijayan
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: abhijithvijayan
otechie: # Replace with a single Otechie username
custom: ['https://www.buymeacoffee.com/abhijithvijayan', 'https://www.paypal.me/iamabhijithvijayan']

BIN
.github/assets/direct-download.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
.github/assets/options-v4-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
.github/assets/popup-v4-1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

82
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,82 @@
name: Build and Deploy
run-name: ${{ github.actor }} started build action
on:
push:
branches:
- master
tags:
- 'v*.*.*'
pull_request:
branches:
- master
# Allows to run this workflow manually from the Actions tab
workflow_dispatch:
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: '${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Build for all browsers
run: npm run build
- name: Upload Chrome extension artifact
uses: actions/upload-artifact@v4
with:
name: chrome-extension
path: extension/chrome.zip
- name: Upload Firefox extension artifact
uses: actions/upload-artifact@v4
with:
name: firefox-extension
path: extension/firefox.xpi
deploy:
needs: build
runs-on: ubuntu-latest
# Only deploy when a tag is pushed (e.g., v1.0.0)
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Build for all browsers
run: npm run build
- name: Deploy to extension branch
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./extension
publish_branch: extension
keep_files: false

105
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# ignore all haters
haters/
# Logs
logs
*.log
@@ -62,4 +65,104 @@ typings/
.DS_Store
## scripts build
extension/
extension/
dist/
# awesome-ts-loader cache
.awcache
# yarn 2
# https://github.com/yarnpkg/berry/issues/454#issuecomment-530312089
.yarn/*
!.yarn/releases
!.yarn/plugins
.pnp.*
### WebStorm+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### WebStorm+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
20

View File

@@ -1,19 +0,0 @@
language: node_js
cache:
directories:
- ~/.npm
node_js:
- stable
git:
depth: 3
script:
- npm run build
deploy:
provider: pages
skip-cleanup: true
keep-history: true
github-token: $GITHUB_TOKEN
local-dir: extension
target-branch: extension
on:
branch: master

View File

@@ -63,14 +63,4 @@ Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
members of the project's leadership.

View File

@@ -1,26 +1,56 @@
## Contributing Guidelines
## Assets
- [kutt.it API](https://github.com/thedevs-network/kutt#api) is used to retreive shortened URLs.
- QR Code is generated using <a href="http://goqr.me/">`http://goqr.me/`</a> API
## Development
- `npm install` to install dependencies.
- `npm run dev` to to watch file changes in developement
- (Reload Extension Manually with reload button in `chrome://extensions`)
- Load extension via `chrome://extensions` as unpacked from `extension/` directory.
- You need to generate an API Key from <a href="https://kutt.it">`https://kutt.it/`</a> (Settings page)
- To watch file changes in developement
- Chrome
- `npm run dev:chrome`
- Firefox
- `npm run dev:firefox`
(Reload Extension Manually in the browser)
- Load extension in browser
- ### Chrome
- Go to the browser address bar and type `chrome://extensions`
- Check the `Developer Mode` button to enable it.
- Click on the `Load Unpacked Extension…` button.
- Select your extensions extracted directory.
<img width="400" src="https://i.imgur.com/dJRL7By.png" />
- ### Firefox
- Load the Add-on via `about:debugging` as temporary Add-on.
- Choose the `manifest.json` file in the extracted directory
<img width="400" src="https://i.imgur.com/aAL5dQg.png" />
- Generate an API Key from <a href="https://kutt.it">`https://kutt.it/`</a> (Settings page)
- Paste and Save the `Key` in extension's `options page`.
`npm run build` builds the extension to `extension/` directory.
`npm run build` builds the extension for all the browsers to `extension/(browser)` directory respectively.
## TODO list
## Testing
- [ ] UI Improvements
- [ ] Custom Domain Support
- [ ] Password
- [x] API Key Page
- [ ] Logo Change
Download latest `Release`
## Note:
Shortening may take a while, it's not the issue with the extension but with <a href="https://github.com/thedevs-network/kutt">Kutt.it's API</a>.
[<img src=".github/assets/direct-download.png"
alt="Direct download"
height="50">](https://github.com/thedevs-network/kutt-extension/releases)
<hr />
## Self-hosted Kutt
- **Enable Developer Options** to use with self-hosted kutt
- Save the self hosted domain in the input (eg: https://mykutt.it)
- **Note**: the api endpoint is automatically appended during the api call.

136
README.md
View File

@@ -1,28 +1,128 @@
<div align="center"><img width="150" src="src/assets/logo.png" /></div>
<h1 align="center">kuttURL-Chrome</h1>
<p align="center">Chrome browser extension to to shorten URLs for <a href="https://kutt.it">Kutt.it</a></p>
<div align="center"><img width="150" src="source/public/assets/logo.png" /></div>
<h1 align="center">kutt-extension</h1>
<p align="center">Browser extension for <a href="https://kutt.it">Kutt.it</a> URL shortener</p>
<div align="center">
<a href="https://github.com/thedevs-network/kutt-extension/actions/workflows/build.yml">
<img src="https://github.com/thedevs-network/kutt-extension/actions/workflows/build.yml/badge.svg?branch=master" alt="Build" />
</a>
<a href="https://github.com/thedevs-network/kutt-extension/releases/latest">
<img src="https://img.shields.io/github/release/thedevs-network/kutt-extension.svg?colorB=blue" alt="Releases" />
</a>
<a href="https://github.com/thedevs-network/kutt-extension/issues?q=is%3Aopen+is%3Aissue">
<img src="https://img.shields.io/github/issues-raw/thedevs-network/kutt-extension.svg?colorB=lightgrey" alt="Open Issues" />
</a>
<a href="https://github.com/thedevs-network/kutt-extension/issues?q=is%3Aissue+is%3Aclosed">
<img src="https://img.shields.io/github/issues-closed-raw/thedevs-network/kutt-extension.svg?colorB=red" alt="Closed Issues" />
</a>
<a href="https://github.com/thedevs-network/kutt-extension/blob/master/license">
<img src="https://img.shields.io/github/license/thedevs-network/kutt-extension.svg" alt="LICENSE" />
</a>
</div>
<hr />
❤️ it? ⭐️ it on [GitHub](https://github.com/thedevs-network/kutt-extension/stargazers)
## Features
- Minimal UI
- Instant QR Code
- Cross Browser Support
- Supports Secure Passwords for URLs
- History & Incognito Feature
- Auto Copy Feature
- Free and Open Source
- Uses WebExtensions API
## Tech Stack
- **Bundler**: [Vite](https://vitejs.dev/) 6
- **UI**: [React](https://react.dev/) 19
- **Language**: [TypeScript](https://www.typescriptlang.org/) 5.7
- **Styling**: SCSS with CSS Modules
- **Linting**: ESLint 9 (flat config) + Prettier
## Browser Support
This extension uses **Manifest V3**.
| [![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png)](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) | [![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png)](https://addons.mozilla.org/firefox/addon/kutt/) | [![Opera](https://raw.github.com/alrra/browser-logos/master/src/opera/opera_48x48.png)](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) | [![Edge](https://raw.github.com/alrra/browser-logos/master/src/edge/edge_48x48.png)](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) | [![Brave](https://raw.github.com/alrra/browser-logos/master/src/brave/brave_48x48.png)](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 88+ | 109+ | 74+ | 88+ | 1.21+ |
## Installation
- **Chrome**: [Kutt :: Chrome Web Store](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd)
- **Firefox**: [Kutt :: Add-ons for Firefox](https://addons.mozilla.org/firefox/addon/kutt/)
- **Edge**: [Kutt :: Chrome Web Store](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd)
## How to Use
1. Generate an API Key from <a href="https://kutt.it">`https://kutt.it/`</a> after signing up (Settings page)
<img width="400" src="https://i.imgur.com/qQwqeH5.png" />
2. Paste and Save this `Key` in extension's `options page` when asked
## Screenshots
<div align="center">
<img width="250" src="https://i.imgur.com/6WKLJQB.gif" alt="image1" />
<img width="300" src="https://i.imgur.com/RVCKF8G.png" alt="image2" />
<img width="250" src="https://i.imgur.com/ju7Vrc5.gif" alt="image3" />
<div>
<img width="250" src="./.github/assets/popup-v4-1.png" alt="popup" />
<div>_</div>
<img width="330" src="./.github/assets/options-v4-1.png" alt="options" />
</div>
Note:
## Development
Until Kutt.it provides CORS Support, a CORS Proxy is used temporarily:
- https://cors-anywhere.herokuapp.com/
Ensure you have [Node.js](https://nodejs.org) 20 or later installed.
## How to use
- Download and Extract the latest zip file from [here](https://github.com/abhijithvijayan/kuttUrl-Chrome-extension/releases/latest)
- Load extension via `chrome://extensions` as unpacked from extracted directory.
- You'll need to generate an API Key from <a href="https://kutt.it">`https://kutt.it/`</a>. (Settings page)
- Paste and Save this `Key` in extension's `options page`.
```bash
# Install dependencies
npm install
# Start development server
npm run dev:chrome # For Chrome
npm run dev:firefox # For Firefox
# Build for production
npm run build:chrome # Build Chrome extension
npm run build:firefox # Build Firefox addon
npm run build # Build for all browsers
# Linting
npm run lint # Run ESLint
npm run lint:fix # Run ESLint with auto-fix
```
### Loading the Extension
#### Chrome
1. Navigate to `chrome://extensions`
2. Enable "Developer mode"
3. Click "Load unpacked"
4. Select `extension/chrome` directory
#### Firefox
1. Navigate to `about:debugging`
2. Click "This Firefox"
3. Click "Load Temporary Add-on"
4. Select `extension/firefox/manifest.json`
## Note
- <a href="https://kutt.it">Kutt.it</a> API permits **50** URLs shortening per day using the API Key
- **Enable Custom Host** option to use with self-hosted kutt
- Save the self hosted domain in the input (eg: `https://mykutt.it`)
- **Note**: the api endpoint is automatically appended during the api call
- _Delay at times while shortening might be the issue with Kutt.it API and not with the extension's_
## Contributing and Support
## How to contribute
View the Contributing guidelines [here](CONTRIBUTING.md).
## Licence
Code released under the [MIT License](LICENSE).
Original Repo: [thedevs-network/kutt](https://github.com/thedevs-network/kutt)
## License
Code released under the [MIT License](license).

48
eslint.config.mjs Normal file
View File

@@ -0,0 +1,48 @@
import nodeConfig from '@abhijithvijayan/eslint-config/node';
import tsConfig from '@abhijithvijayan/eslint-config/typescript';
import reactConfig from '@abhijithvijayan/eslint-config/react';
export default [
{
ignores: [
'node_modules/**',
'dist/**',
'extension/**',
'.yarn/**',
'.pnp.js',
'*.js',
'*.mjs',
'vite.config.ts',
],
},
...nodeConfig({
files: ['**/*.ts', '**/*.tsx'],
}),
...tsConfig({
files: ['**/*.ts', '**/*.tsx'],
}),
...reactConfig({
files: ['**/*.tsx'],
}),
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-console': 'off',
'@typescript-eslint/no-use-before-define': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
// Disable due to resolver issues in ESM
'import-x/no-duplicates': 'off',
// Browser extension code uses browser APIs, not Node.js
'n/no-unsupported-features/node-builtins': 'off',
},
},
{
files: ['**/*.tsx'],
rules: {
'react/jsx-props-no-spreading': 'off',
'react/react-in-jsx-scope': 'off',
'react/no-array-index-key': 'warn',
'jsx-a11y/label-has-associated-control': 'off',
},
},
];

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019 Abhijith V
Copyright (c) Abhijith Vijayan <email@abhijithvijayan.in> (https://abhijithvijayan.in)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

16260
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,76 @@
{
"name": "kutturl",
"version": "1.0.0",
"main": "background.js",
"scripts": {
"dev": "webpack --watch --mode development",
"build": "webpack --mode production",
"start": "webpack-dev-server --mode development --open"
},
"author": "abhijithvijayan",
"name": "kutt-extension",
"version": "4.4.2",
"description": "Kutt.it extension for browsers.",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.3",
"autoprefixer": "^9.4.4",
"babel-loader": "^8.0.4",
"clean-webpack-plugin": "^1.0.0",
"copy-webpack-plugin": "^4.6.0",
"css-loader": "^2.1.0",
"extract-loader": "^3.1.0",
"file-loader": "^3.0.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"node-sass": "^4.11.0",
"postcss-loader": "^3.0.0",
"precss": "^4.0.0",
"resolve-url-loader": "^3.0.0",
"sass-loader": "^7.1.0",
"url-loader": "^1.1.2",
"webpack": "^4.28.3",
"webpack-cli": "^3.2.0",
"webpack-dev-server": "^3.1.14"
"repository": "https://github.com/thedevs-network/kutt-extension.git",
"author": {
"name": "abhijithvijayan",
"email": "email@abhijithvijayan.in",
"url": "https://abhijithvijayan.in"
},
"dependencies": {
"axios": "^0.18.0",
"babel-polyfill": "^6.26.0"
"engines": {
"node": ">=20"
},
"description": "<div align=\"center\"><img width=\"100\" src=\"src/assets/logo.png\" /></div>\r <h1 align=\"center\">kuttURL-Chrome</h1>\r <p align=\"center\">Chrome extension to to shorten URLs</p>",
"repository": {
"type": "git",
"url": "git+https://github.com/abhijithvijayan/kuttUrl-Chrome.git"
"type": "module",
"scripts": {
"dev:chrome": "cross-env TARGET_BROWSER=chrome vite build --config vite.config.ts --mode development --watch",
"dev:firefox": "cross-env TARGET_BROWSER=firefox vite build --config vite.config.ts --mode development --watch",
"build:chrome": "cross-env TARGET_BROWSER=chrome vite build --config vite.config.ts",
"build:firefox": "cross-env TARGET_BROWSER=firefox vite build --config vite.config.ts",
"build": "npm run build:chrome && npm run build:firefox",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"keywords": [
"url",
"shortener"
"shortener",
"browser",
"extension",
"addon",
"kutt"
],
"bugs": {
"url": "https://github.com/abhijithvijayan/kuttUrl-Chrome/issues"
"private": true,
"dependencies": {
"@abhijithvijayan/ts-utils": "^1.2.2",
"advanced-css-reset": "^2.1.3",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^19.0.0",
"webextension-polyfill": "^0.12.0"
},
"homepage": "https://github.com/abhijithvijayan/kuttUrl-Chrome#readme"
"devDependencies": {
"@abhijithvijayan/eslint-config": "^3.0.1",
"@abhijithvijayan/tsconfig": "^1.5.1",
"@types/node": "^25.0.3",
"@types/react": "^19.0.2",
"@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^19.0.2",
"@types/webextension-polyfill": "^0.12.1",
"@typescript-eslint/eslint-plugin": "^8.51.0",
"@typescript-eslint/parser": "^8.51.0",
"@vitejs/plugin-react": "^5.1.2",
"autoprefixer": "^10.4.20",
"cross-env": "^10.1.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-n": "^17.23.1",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"globals": "^17.0.0",
"postcss": "^8.4.49",
"prettier": "^3.7.4",
"sass": "^1.83.0",
"typescript": "^5.7.2",
"vite": "^7.3.0",
"vite-plugin-checker": "^0.12.0",
"vite-plugin-wext-manifest": "^1.2.2",
"vite-plugin-zip-pack": "^1.2.4"
}
}

5
postcss.config.js Normal file
View File

@@ -0,0 +1,5 @@
import autoprefixer from 'autoprefixer';
export default {
plugins: [autoprefixer()],
};

View File

@@ -0,0 +1,8 @@
export const CHECK_API_KEY = 'api.checkApiKey';
export const CHECK_API_KEY_TIMEOUT = 8000; // 8secs
export const SHORTEN_URL = 'api.shortenUrl';
export const SHORTEN_URL_TIMEOUT = 20000; // 20secs
export const FETCH_URLS_HISTORY = 'api.fetchUrlsHistory';
export const MAX_HISTORY_ITEMS = 15;

358
source/Background/index.ts Normal file
View File

@@ -0,0 +1,358 @@
/**
* kutt-extension
*
* @author abhijithvijayan <abhijithvijayan.in>
* @license MIT License
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import browser, {Runtime} from 'webextension-polyfill';
import axios, {AxiosPromise, AxiosError} from 'axios';
import * as constants from './constants';
export enum Kutt {
hostDomain = 'kutt.it',
hostUrl = 'https://kutt.it',
}
export enum StoreLinks {
chrome = 'https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd/reviews',
firefox = 'https://addons.mozilla.org/en-US/firefox/addon/kutt/reviews/',
}
// **** ------------------ **** //
export type ErrorStateProperties = {
error: boolean | null;
message: string;
};
export type ApiErroredProperties = {
error: true;
message: string;
};
export type AuthRequestBodyProperties = {
apikey: string;
hostUrl: HostUrlProperties;
};
// **** ------------------ **** //
type HostUrlProperties = string;
export type DomainEntryProperties = {
address: string;
banned: boolean;
created_at: string;
id: string;
homepage: string;
updated_at: string;
};
type ShortenUrlBodyProperties = {
target: string;
password?: string;
customurl?: string;
reuse: boolean;
domain?: string;
};
type ShortenLinkResponseProperties = {
id: string;
address: string;
banned: boolean;
password: boolean;
target: string;
visit_count: number;
created_at: string;
updated_at: string;
link: string;
};
export interface ApiBodyProperties extends ShortenUrlBodyProperties {
apikey: string;
}
export type ShortUrlActionBodyProperties = {
apiBody: ApiBodyProperties;
hostUrl: HostUrlProperties;
};
export type SuccessfulShortenStatusProperties = {
error: false;
data: ShortenLinkResponseProperties;
};
/**
* Shorten URL using v2 API
*/
async function shortenUrl({
apiBody,
hostUrl,
}: ShortUrlActionBodyProperties): Promise<
SuccessfulShortenStatusProperties | ApiErroredProperties
> {
try {
const {apikey, ...otherParams} = apiBody;
const {data}: {data: ShortenLinkResponseProperties} = await axios({
method: 'POST',
timeout: constants.SHORTEN_URL_TIMEOUT,
url: `${hostUrl}/api/v2/links`,
headers: {
'X-API-Key': apikey,
},
data: {
...otherParams,
},
// Prevent cookies from being sent to avoid session-based auth conflicts
withCredentials: false,
});
return {
error: false,
data,
};
} catch (error) {
const err = error as AxiosError<{error?: string}>;
if (err.response) {
if (err.response.status === 401) {
return {
error: true,
message: 'Error: Invalid API Key',
};
}
// server request validation errors
if (
err.response.status === 400 &&
Object.prototype.hasOwnProperty.call(err.response.data, 'error')
) {
return {
error: true,
message: `Error: ${err.response.data?.error}`,
};
}
// ToDo: remove in the next major update
if (err.response.status === 404) {
return {
error: true,
message:
'Error: This extension now uses API v2, please update your kutt.it instance.',
};
}
}
if (err.code === 'ECONNABORTED') {
return {
error: true,
message: 'Error: Timed out',
};
}
return {
error: true,
message: 'Error: Something went wrong',
};
}
}
// **** ------------------ **** //
export type UserSettingsResponseProperties = {
apikey: string;
email: string;
domains: DomainEntryProperties[];
};
export type SuccessfulApiKeyCheckProperties = {
error: false;
data: UserSettingsResponseProperties;
};
function getUserSettings({
apikey,
hostUrl,
}: AuthRequestBodyProperties): AxiosPromise<any> {
return axios({
method: 'GET',
url: `${hostUrl}/api/v2/users`,
timeout: constants.CHECK_API_KEY_TIMEOUT,
headers: {
'X-API-Key': apikey,
},
// Prevent cookies from being sent to avoid session-based auth conflicts
withCredentials: false,
});
}
async function checkApiKey({
apikey,
hostUrl,
}: AuthRequestBodyProperties): Promise<
SuccessfulApiKeyCheckProperties | ApiErroredProperties
> {
try {
const {data}: {data: UserSettingsResponseProperties} =
await getUserSettings({
apikey,
hostUrl,
});
return {
error: false,
data,
};
} catch (error) {
const err = error as AxiosError;
if (err.response) {
if (err.response.status === 401) {
return {
error: true,
message: 'Error: Invalid API Key',
};
}
return {
error: true,
message: 'Error: Something went wrong.',
};
}
if (err.code === 'ECONNABORTED') {
return {
error: true,
message: 'Error: Timed out',
};
}
return {
error: true,
message: 'Error: Requesting to server failed.',
};
}
}
// **** ------------------ **** //
export type SuccessfulUrlsHistoryFetchProperties = {
error: false;
data: UserShortenedLinksHistoryResponseBody;
};
type UserShortenedLinksHistoryResponseBody = {
limit: number;
skip: number;
total: number;
data: UserShortenedLinkStats[];
};
export type UserShortenedLinkStats = {
address: string;
banned: boolean;
created_at: string;
id: string;
link: string;
password: boolean;
target: string;
updated_at: string;
visit_count: number;
};
/**
* Fetch User's recent 15 shortened urls
*/
async function fetchUrlsHistory({
apikey,
hostUrl,
}: AuthRequestBodyProperties): Promise<
SuccessfulUrlsHistoryFetchProperties | ApiErroredProperties
> {
try {
const {data}: {data: UserShortenedLinksHistoryResponseBody} = await axios({
method: 'GET',
timeout: constants.SHORTEN_URL_TIMEOUT,
url: `${hostUrl}/api/v2/links`,
params: {
limit: constants.MAX_HISTORY_ITEMS,
},
headers: {
'X-API-Key': apikey,
},
// Prevent cookies from being sent to avoid session-based auth conflicts
withCredentials: false,
});
return {
error: false,
data,
};
} catch (error) {
const err = error as AxiosError;
if (err.response) {
if (err.response.status === 401) {
return {
error: true,
message: 'Error: Invalid API Key',
};
}
return {
error: true,
message: 'Error: Something went wrong.',
};
}
if (err.code === 'ECONNABORTED') {
return {
error: true,
message: 'Error: Timed out',
};
}
return {
error: true,
message: 'Error: Requesting to server failed.',
};
}
}
// **** ------------------ **** //
/**
* Service worker installation listener (MV3)
*/
browser.runtime.onInstalled.addListener((): void => {
console.log('Kutt extension installed');
});
type MessageRequest = {
action: string;
params: any;
};
/**
* Listen for messages from UI pages
*/
browser.runtime.onMessage.addListener(
(message: unknown, _sender: Runtime.MessageSender): void | Promise<any> => {
const request = message as MessageRequest;
switch (request.action) {
case constants.CHECK_API_KEY: {
return checkApiKey(request.params);
}
case constants.SHORTEN_URL: {
return shortenUrl(request.params);
}
case constants.FETCH_URLS_HISTORY: {
return fetchUrlsHistory(request.params);
}
}
}
);

View File

@@ -0,0 +1,36 @@
@use '../styles/variables' as *;
.historyPage {
height: 100vh;
overflow: hidden;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
}
.historyContent {
display: flex;
flex-direction: column;
height: 100%;
padding: 2rem 1.5rem;
max-width: 1400px;
margin: 0 auto;
}
.errorMessage {
text-align: center;
font-size: 1rem;
font-weight: $medium;
color: $gray-600;
padding: 2rem;
background-color: $white;
border-radius: $radius-lg;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
margin-top: 1.5rem;
}
.loaderContainer {
display: flex;
justify-content: center;
align-items: center;
height: 10rem;
margin-top: 1.5rem;
}

160
source/History/History.tsx Normal file
View File

@@ -0,0 +1,160 @@
import type {JSX} from 'react';
import {useEffect, useState} from 'react';
import {
useShortenedLinks,
ShortenedLinksActionTypes,
} from '../contexts/shortened-links-context';
import {
HostProperties,
useExtensionSettings,
} from '../contexts/extension-settings-context';
import {
useRequestStatus,
RequestStatusActionTypes,
} from '../contexts/request-status-context';
import messageUtil from '../util/messageUtil';
import {FETCH_URLS_HISTORY} from '../Background/constants';
import {getExtensionSettings} from '../util/settings';
import {
SuccessfulUrlsHistoryFetchProperties,
AuthRequestBodyProperties,
ApiErroredProperties,
ErrorStateProperties,
Kutt,
} from '../Background';
import {isValidUrl} from '../util/link';
import BodyWrapper from '../components/BodyWrapper';
import Loader from '../components/Loader';
import Header from '../Options/Header';
import Table from './Table';
import styles from './History.module.scss';
function History(): JSX.Element {
const [, shortenedLinksDispatch] = useShortenedLinks();
const [, extensionSettingsDispatch] = useExtensionSettings();
const [requestStatusState, requestStatusDispatch] = useRequestStatus();
const [errored, setErrored] = useState<ErrorStateProperties>({
error: null,
message: '',
});
const [hostUrl, setHostUrl] = useState<string>(Kutt.hostUrl);
useEffect(() => {
async function getUrlsHistoryStats(): Promise<void> {
// ********************************* //
// **** GET EXTENSIONS SETTINGS **** //
// ********************************* //
const {settings = {}} = await getExtensionSettings();
const advancedSettings: boolean =
(settings?.advanced as boolean) || false;
const defaultHost: HostProperties =
(advancedSettings &&
(settings?.host as string) &&
isValidUrl(settings.host as string) && {
hostDomain:
(settings.host as string)
.replace('http://', '')
.replace('https://', '')
.replace('www.', '')
.split(/[/?#]/)[0] || '', // extract domain
hostUrl: (settings.host as string).endsWith('/')
? (settings.host as string).slice(0, -1)
: (settings.host as string), // slice `/` at the end
}) ||
Kutt;
// inject existing keys (if field doesn't exist, use default)
const defaultExtensionConfig = {
apikey: (settings?.apikey as string)?.trim() || '',
history: (settings?.history as boolean) || false,
advanced:
defaultHost.hostUrl.trim() !== Kutt.hostUrl && advancedSettings, // disable `advanced` if customhost is not set
host: defaultHost,
};
setHostUrl(defaultExtensionConfig.host.hostUrl);
// extensionSettingsDispatch({
// type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS,
// payload: defaultExtensionConfig,
// });
if (defaultExtensionConfig.history) {
// ****************************************************** //
// **************** FETCH URLS HISTORY ****************** //
// ****************************************************** //
const urlsHistoryFetchRequetBody: AuthRequestBodyProperties = {
apikey: defaultExtensionConfig.apikey,
hostUrl: defaultExtensionConfig.host.hostUrl,
};
// call api
const response:
| SuccessfulUrlsHistoryFetchProperties
| ApiErroredProperties = await messageUtil.send(
FETCH_URLS_HISTORY,
urlsHistoryFetchRequetBody
);
if (!response.error) {
setErrored({error: false, message: 'Fetch successful'});
shortenedLinksDispatch({
type: ShortenedLinksActionTypes.HYDRATE_SHORTENED_LINKS,
payload: {
items: response.data.data,
total: response.data.total,
},
});
} else {
setErrored({error: true, message: response.message});
}
} else {
setErrored({
error: true,
message: 'History page disabled. Please enable it from settings.',
});
}
requestStatusDispatch({
type: RequestStatusActionTypes.SET_LOADING,
payload: false,
});
}
getUrlsHistoryStats();
}, [
extensionSettingsDispatch,
requestStatusDispatch,
shortenedLinksDispatch,
]);
return (
<BodyWrapper>
<div id="history" className={styles.historyPage}>
<div className={styles.historyContent}>
<Header subtitle="Recent Links" hostUrl={hostUrl} />
{}
{!requestStatusState.loading ? (
!errored.error ? (
<Table />
) : (
<h2 className={styles.errorMessage}>{errored.message}</h2>
)
) : (
<div className={styles.loaderContainer}>
<Loader />
</div>
)}
</div>
</div>
</BodyWrapper>
);
}
export default History;

View File

@@ -0,0 +1,63 @@
@use '../styles/variables' as *;
.modalOverlay {
position: fixed;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
z-index: 1000;
}
.modalContent {
padding: 2.5rem 3rem;
text-align: center;
background-color: $white;
border-radius: $radius-lg;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.qrCodeWrapper {
display: flex;
justify-content: center;
padding: 1rem;
background-color: $white;
border-radius: $radius-md;
}
.buttonWrapper {
display: flex;
justify-content: center;
margin-top: 1.5rem;
}
.closeButton {
display: flex;
align-items: center;
justify-content: center;
height: 2.5rem;
padding: 0 1.5rem;
font-size: 0.875rem;
font-weight: $medium;
color: $white;
cursor: pointer;
border-radius: $radius-md;
border: none;
background: $primary-gradient;
box-shadow: 0 4px 6px -1px rgba(126, 87, 194, 0.3);
transition: transform $transition-fast, box-shadow $transition-fast;
&:hover {
transform: translateY(-1px);
box-shadow: 0 6px 10px -1px rgba(126, 87, 194, 0.4);
}
&:active {
transform: translateY(0);
}
}

50
source/History/Modal.tsx Normal file
View File

@@ -0,0 +1,50 @@
import {QRCodeSVG} from 'qrcode.react';
import type {JSX} from 'react';
import {Dispatch, SetStateAction} from 'react';
import styles from './Modal.module.scss';
type Props = {
link: string;
setModalView: Dispatch<SetStateAction<boolean>>;
};
function Modal({link, setModalView}: Props): JSX.Element {
return (
<>
<div
className={styles.modalOverlay}
onClick={(): void => setModalView(false)}
onKeyDown={(e): void => {
if (e.key === 'Escape') setModalView(false);
}}
role="button"
tabIndex={0}
>
<div
className={styles.modalContent}
onClick={(e): void => e.stopPropagation()}
onKeyDown={(e): void => e.stopPropagation()}
role="button"
tabIndex={0}
>
<div className={styles.qrCodeWrapper}>
<QRCodeSVG size={196} value={link} />
</div>
<div className={styles.buttonWrapper}>
<button
onClick={(): void => setModalView(false)}
className={styles.closeButton}
type="button"
>
Close
</button>
</div>
</div>
</div>
</>
);
}
export default Modal;

View File

@@ -0,0 +1,227 @@
@use '../styles/variables' as *;
.tableContainer {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
flex: 1;
min-height: 0;
margin-top: 1.5rem;
}
.tableWrapper {
display: flex;
flex-direction: column;
width: 100%;
max-width: 1200px;
flex: 1;
min-height: 0;
}
.tableHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.tableTitle {
font-size: 0.875rem;
font-weight: $medium;
color: $gray-600;
margin: 0;
}
.table {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
background-color: $white;
border-radius: $radius-lg;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.thead {
display: flex;
flex-direction: column;
background-color: $gray-200;
}
.theadRow {
display: flex;
justify-content: space-between;
padding: 0 1.5rem;
border-bottom: 1px solid $gray-300;
}
.th {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 0.875rem 0;
font-size: 0.75rem;
font-weight: $semibold;
text-transform: uppercase;
letter-spacing: 0.05em;
color: $gray-600;
line-height: normal;
}
.thOriginal {
flex: 2 2 0px;
}
.thShort {
flex: 1 1 0px;
}
.tbody {
display: flex;
flex-direction: column;
overflow-y: auto;
}
.tr {
display: flex;
justify-content: space-between;
padding: 0 1.5rem;
border-bottom: 1px solid $gray-200;
transition: background-color $transition-fast;
&:last-child {
border-bottom: none;
}
&:hover {
background-color: rgba($gray-200, 0.3);
}
}
.td {
position: relative;
display: flex;
align-items: center;
padding: 1rem 0;
}
.tdOriginal {
position: relative;
overflow: hidden;
white-space: nowrap;
flex: 2 2 0px;
padding-right: 1rem;
&::after {
content: '';
position: absolute;
right: 0px;
top: 0px;
height: 100%;
width: 48px;
background: linear-gradient(to left, $white, $white, transparent);
pointer-events: none;
}
.tr:hover &::after {
background: linear-gradient(to left, rgba($gray-200, 0.3), rgba($gray-200, 0.3), transparent);
}
}
.tdShort {
position: relative;
overflow: hidden;
white-space: nowrap;
flex: 1 1 0px;
padding-right: 1rem;
&::after {
content: '';
position: absolute;
right: 0px;
top: 0px;
height: 100%;
width: 48px;
background: linear-gradient(to left, $white, $white, transparent);
pointer-events: none;
}
.tr:hover &::after {
background: linear-gradient(to left, rgba($gray-200, 0.3), rgba($gray-200, 0.3), transparent);
}
}
.link {
font-size: 0.875rem;
line-height: normal;
text-decoration: none;
color: $blue-500;
transition: color $transition-fast;
&:hover {
color: darken($blue-500, 10%);
text-decoration: underline;
}
}
.copiedNotification {
position: absolute;
top: 0.25rem;
left: 0;
font-size: 0.625rem;
font-weight: $medium;
color: $green-500;
background-color: rgba($green-500, 0.1);
padding: 0.125rem 0.375rem;
border-radius: $radius-sm;
}
.shortUrlWrapper {
display: flex;
align-items: center;
}
.actionsWrapper {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
}
.actionIcon {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
background-color: $gray-200;
border-radius: $radius-full;
border: none;
outline: none;
cursor: pointer;
transition: transform $transition-fast, background-color $transition-fast;
&:hover {
transform: translateY(-2px);
background-color: $gray-300;
}
svg {
stroke: $gray-600;
stroke-width: 2;
}
&:first-child svg {
stroke: $green-500;
}
}
.emptyRow {
padding: 2rem;
color: $gray-500;
text-align: center;
font-size: 0.875rem;
}

153
source/History/Table.tsx Normal file
View File

@@ -0,0 +1,153 @@
import CopyToClipboard from 'react-copy-to-clipboard';
import type {JSX} from 'react';
import {useEffect, useState} from 'react';
import clsx from 'clsx';
import {
useShortenedLinks,
ShortenedLinksActionTypes,
} from '../contexts/shortened-links-context';
import {MAX_HISTORY_ITEMS} from '../Background/constants';
import Icon from '../components/Icon';
import Modal from './Modal';
import styles from './Table.module.scss';
function Table(): JSX.Element {
const [shortenedLinksState, shortenedLinksDispatch] = useShortenedLinks();
const [QRView, setQRView] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false);
// reset copy message
useEffect(() => {
let timer: ReturnType<typeof setTimeout> | null = null;
timer = setTimeout(() => {
setCopied(false);
// reset selected id from context
}, 1300);
return (): void => {
if (timer) {
clearTimeout(timer);
}
};
}, [copied]);
function handleCopyToClipboard(selectedItemId: string): void {
shortenedLinksDispatch({
type: ShortenedLinksActionTypes.SET_CURRENT_SELECTED,
payload: selectedItemId,
});
setCopied(true);
}
function handleQRCodeViewToggle(selectedItemId: string): void {
shortenedLinksDispatch({
type: ShortenedLinksActionTypes.SET_CURRENT_SELECTED,
payload: selectedItemId,
});
setQRView(true);
}
return (
<>
<div className={styles.tableContainer}>
<div className={styles.tableWrapper}>
<div className={styles.tableHeader}>
<h2 className={styles.tableTitle}>
Recent shortened links. (last {MAX_HISTORY_ITEMS} results)
</h2>
</div>
<table className={styles.table}>
<thead className={styles.thead}>
<tr className={styles.theadRow}>
<th className={clsx(styles.th, styles.thOriginal)}>
Original URL
</th>
<th className={clsx(styles.th, styles.thShort)}>Short URL</th>
</tr>
</thead>
<tbody className={styles.tbody}>
{!(shortenedLinksState.total === 0) ? (
shortenedLinksState.items.map((item) => (
<tr key={item.id} className={styles.tr}>
<td className={clsx(styles.td, styles.tdOriginal)}>
<a
className={styles.link}
href={item.target}
target="_blank"
rel="noopener noreferrer nofollow"
>
{item.target}
</a>
</td>
<td className={clsx(styles.td, styles.tdShort)}>
{copied &&
shortenedLinksState.selected?.id === item.id && (
<div className={styles.copiedNotification}>
Copied to clipboard!
</div>
)}
<div className={styles.shortUrlWrapper}>
<a
className={styles.link}
href={item.link}
target="_blank"
rel="noopener noreferrer nofollow"
>
{item.link}
</a>
</div>
</td>
<td className={styles.td}>
<div className={styles.actionsWrapper}>
{/* // **** COPY TO CLIPBOARD **** // */}
{copied &&
shortenedLinksState.selected?.id === item.id ? (
<Icon name="tick" className={styles.actionIcon} />
) : (
<CopyToClipboard
text={item.link}
onCopy={(): void => handleCopyToClipboard(item.id)}
>
<Icon name="copy" className={styles.actionIcon} />
</CopyToClipboard>
)}
<Icon
onClick={(): void => handleQRCodeViewToggle(item.id)}
className={styles.actionIcon}
name="qrcode"
/>
</div>
{/* // **** QR CODE MODAL **** // */}
{QRView &&
shortenedLinksState.selected?.id === item.id && (
<Modal link={item.link} setModalView={setQRView} />
)}
</td>
</tr>
))
) : (
<tr>
<td className={styles.emptyRow}>No URLs History</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</>
);
}
export default Table;

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>History: Kutt</title>
</head>
<body>
<div id="history-root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>

27
source/History/index.tsx Normal file
View File

@@ -0,0 +1,27 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import {ExtensionSettingsProvider} from '../contexts/extension-settings-context';
import {RequestStatusProvider} from '../contexts/request-status-context';
import {ShortenedLinksProvider} from '../contexts/shortened-links-context';
import History from './History';
import './styles.scss';
const container = document.getElementById('history-root');
if (!container) {
throw new Error('Could not find history-root container');
}
const root = createRoot(container);
root.render(
<StrictMode>
<ExtensionSettingsProvider>
<RequestStatusProvider>
<ShortenedLinksProvider>
<History />
</ShortenedLinksProvider>
</RequestStatusProvider>
</ExtensionSettingsProvider>
</StrictMode>
);

View File

@@ -0,0 +1,5 @@
@use '../styles/main.scss';
body {
background-color: #edf2f7;
}

View File

@@ -0,0 +1,108 @@
@use '../styles/variables' as *;
.footer {
margin-top: 1.25rem;
padding-top: 1rem;
font-weight: $regular;
font-size: 0.75rem;
}
.ratingSection {
display: flex;
align-items: center;
color: $gray-800;
}
.dividerLine {
display: block;
width: 33.333%;
border: 1px solid $gray-200;
&.left {
margin-right: 0.5rem;
}
&.right {
margin-left: 0.5rem;
}
}
.ratingLink {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-decoration: none;
color: inherit;
}
.starsContainer {
display: flex;
align-items: center;
margin-top: 0.25rem;
&:hover .starIcon {
transform: scale(1.1);
}
}
.starIcon {
margin-right: 0.25rem;
fill: currentColor;
transition: transform $transition-normal, color $transition-normal;
&.yellow {
color: $yellow-500;
}
&.gray {
color: $gray-400;
&:last-child {
margin-right: 0;
}
}
.ratingLink:hover &.gray {
color: $yellow-500;
}
}
.ratingText {
margin-bottom: 0;
margin-top: 0.25rem;
}
.linksSection {
display: flex;
align-items: center;
justify-content: space-around;
text-align: center;
margin-top: 1rem;
}
.linkItem {
padding: 0.25rem;
cursor: pointer;
color: $gray-500;
text-decoration: none;
transition: color $transition-fast;
&:hover {
color: $blue-500;
}
&.narrow {
width: 33.333%;
}
&.wide {
width: 66.666%;
}
}
.linkDivider {
width: 1px;
height: 1rem;
background-color: $gray-300;
}

88
source/Options/Footer.tsx Normal file
View File

@@ -0,0 +1,88 @@
import type {JSX} from 'react';
import {memo} from 'react';
import clsx from 'clsx';
import {detectBrowser} from '../util/browser';
import {StoreLinks} from '../Background';
import Icon from '../components/Icon';
import styles from './Footer.module.scss';
function Footer(): JSX.Element {
return (
<>
<footer className={styles.footer}>
<div className={styles.ratingSection}>
<span className={clsx(styles.dividerLine, styles.left)} />
<a
href={
detectBrowser() === 'firefox'
? StoreLinks.firefox
: StoreLinks.chrome
}
target="_blank"
rel="nofollow noopener noreferrer"
className={styles.ratingLink}
>
<div className={styles.starsContainer}>
<Icon
className={clsx(styles.starIcon, styles.gray)}
name="star-white"
/>
<Icon
className={clsx(styles.starIcon, styles.gray)}
name="star-white"
/>
<Icon
className={clsx(styles.starIcon, styles.gray)}
name="star-white"
/>
<Icon
className={clsx(styles.starIcon, styles.gray)}
name="star-white"
/>
<Icon
className={clsx(styles.starIcon, styles.gray)}
name="star-white"
/>
</div>
<p className={styles.ratingText}>Rate on Store</p>
</a>
<span className={clsx(styles.dividerLine, styles.right)} />
</div>
<div className={styles.linksSection}>
<a
href="https://kutt.it"
target="_blank"
rel="nofollow noopener noreferrer"
className={clsx(styles.linkItem, styles.narrow)}
>
Kutt.it
</a>
<span className={styles.linkDivider} />
<a
href="https://git.io/Jn5hS"
target="_blank"
rel="nofollow noopener noreferrer"
className={clsx(styles.linkItem, styles.wide)}
>
Report an issue
</a>
<span className={styles.linkDivider} />
<a
href="https://github.com/thedevs-network/kutt-extension"
target="_blank"
rel="nofollow noopener noreferrer"
className={clsx(styles.linkItem, styles.narrow)}
>
GitHub
</a>
</div>
</footer>
</>
);
}
export default memo(Footer);

View File

@@ -0,0 +1,540 @@
@use '../styles/variables' as *;
.formSection {
margin-top: 1.25rem;
}
.inputGroup {
display: flex;
flex-direction: column;
font-size: 0.875rem;
}
.label {
margin-bottom: 0.5rem;
font-weight: $semibold;
color: $gray-700;
font-size: 0.8125rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.labelLink {
margin-left: 0.5rem;
color: $blue-500;
text-decoration: none;
text-transform: lowercase;
letter-spacing: normal;
font-weight: $medium;
&:hover {
text-decoration: underline;
}
}
.labelLinkWrapper {
position: relative;
display: inline-flex;
align-items: center;
&:hover .tooltip {
visibility: visible;
opacity: 1;
}
}
.labelWithInfo {
display: inline-flex;
align-items: center;
gap: 0.375rem;
}
.inputWrapper {
position: relative;
}
.inputIconWrapper {
position: absolute;
top: 0;
right: 0;
display: flex;
width: 2.5rem;
height: 100%;
border: 1px solid transparent;
}
.inputIcon {
z-index: 10;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: $gray-600;
border-radius: $radius-sm 0 0 $radius-sm;
}
.input {
position: relative;
width: 100%;
padding: 0.625rem 0.75rem;
padding-right: 3rem;
font-size: 0.875rem;
background-color: $white;
border: 1.5px solid $gray-300;
border-radius: $radius-md;
transition: border-color $transition-fast, box-shadow $transition-fast;
&::placeholder {
color: $gray-400;
}
&:hover {
border-color: $gray-500;
}
&:focus {
outline: none;
border-color: $indigo-400;
box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.2);
}
@media (min-width: 640px) {
font-size: 0.9375rem;
}
}
.inputError {
border-color: $red-500;
}
.errorText {
display: flex;
align-items: center;
margin-top: 0.25rem;
margin-left: 0.25rem;
font-size: 0.75rem;
font-weight: $medium;
letter-spacing: 0.025em;
color: $red-500;
}
.validateSection {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.validateButton {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
margin-top: 1rem;
font-size: 0.8125rem;
font-weight: $semibold;
text-align: center;
color: $white;
background: $primary-gradient;
border: none;
border-radius: $radius-md;
box-shadow: 0 4px 6px -1px rgba(126, 87, 194, 0.3);
cursor: pointer;
transition: transform $transition-fast, box-shadow $transition-fast, opacity $transition-fast;
&:hover {
transform: translateY(-1px);
box-shadow: 0 6px 10px -1px rgba(126, 87, 194, 0.4);
}
&:active {
transform: translateY(0);
}
&:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(126, 87, 194, 0.3);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
}
.validateText {}
.validateIcon {
display: inline-flex;
padding: 0;
background-color: transparent;
svg {
stroke: currentColor;
stroke-width: 2;
transition: transform $transition-normal;
}
}
.validationFeedback {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 0.875rem;
border-radius: $radius-md;
animation: slideIn $transition-normal ease-out;
&.error {
background-color: rgba($red-500, 0.1);
border: 1px solid rgba($red-500, 0.2);
}
&.success {
background-color: rgba($green-500, 0.1);
border: 1px solid rgba($green-500, 0.2);
}
}
.feedbackIcon {
flex-shrink: 0;
svg {
stroke-width: 2;
width: 1rem;
height: 1rem;
}
.error & svg {
stroke: $red-500;
}
.success & svg {
stroke: $green-500;
}
}
.feedbackMessage {
font-size: 0.8125rem;
font-weight: $medium;
line-height: 1.4;
.error & {
color: $red-500;
}
.success & {
color: $green-500;
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.toggleSection {
display: flex;
flex-direction: column;
margin-top: 1.5rem;
padding-top: 1.25rem;
border-top: 1px solid $gray-200;
}
.toggleLabel {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.625rem 0;
cursor: pointer;
border-radius: $radius-sm;
transition: background-color $transition-fast;
&:hover {
background-color: rgba($gray-200, 0.5);
margin: 0 -0.5rem;
padding: 0.625rem 0.5rem;
}
}
.toggleText {
font-size: 0.875rem;
}
.toggleTextWithInfo {
display: flex;
align-items: center;
gap: 0.375rem;
}
.infoIcon {
position: relative;
display: inline-flex;
color: $gray-500;
cursor: help;
&:hover {
color: $gray-700;
}
&:hover .tooltip {
visibility: visible;
opacity: 1;
}
}
.tooltip {
visibility: hidden;
opacity: 0;
position: absolute;
left: 50%;
bottom: calc(100% + 0.5rem);
transform: translateX(-50%);
width: max-content;
max-width: 200px;
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
font-weight: $regular;
line-height: 1.4;
color: $white;
background-color: $gray-800;
border-radius: $radius-sm;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: opacity $transition-normal, visibility $transition-normal;
z-index: 100;
pointer-events: none;
text-transform: none;
letter-spacing: normal;
&::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 0.375rem;
border-style: solid;
border-color: $gray-800 transparent transparent transparent;
}
}
.toggleWrapper {
position: relative;
width: 2.5rem;
height: 1.5rem;
margin-left: 0.75rem;
flex-shrink: 0;
}
.toggleTrack {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: $gray-400;
border-radius: 9999px;
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
transition: background-color $transition-normal;
}
.toggleKnob {
position: absolute;
top: 0.25rem;
left: 0.25rem;
width: 1rem;
height: 1rem;
border-radius: 9999px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
transition: transform $transition-normal;
background-color: $white;
&.active {
transform: translateX(1rem);
}
}
.toggleWrapper:has(.toggleKnob.active) .toggleTrack {
background-color: $purple-600;
}
.toggleInput {
position: absolute;
width: 0;
height: 0;
opacity: 0;
}
.advancedSection {
margin-top: 0.75rem;
padding: 1rem;
background-color: rgba($gray-200, 0.5);
border-radius: $radius-md;
border: 1px solid $gray-200;
max-height: 200px;
opacity: 1;
overflow: visible;
transition: max-height $transition-normal, opacity $transition-normal, margin-top $transition-normal, padding $transition-normal;
&.hidden {
max-height: 0;
opacity: 0;
margin-top: 0;
padding: 0 1rem;
border-color: transparent;
overflow: hidden;
}
}
.resetSection {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
margin-top: 1.5rem;
padding-top: 1.25rem;
border-top: 1px solid $gray-200;
}
.resetButton {
padding: 0.5rem 1rem;
font-size: 0.8125rem;
font-weight: $medium;
color: $red-500;
background-color: transparent;
border: 1.5px solid $red-500;
border-radius: $radius-md;
cursor: pointer;
transition: background-color $transition-fast, color $transition-fast;
&:hover {
background-color: $red-500;
color: $white;
}
}
.resetHint {
font-size: 0.75rem;
color: $gray-500;
}
.modalOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn $transition-fast ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal {
background-color: $white;
border-radius: $radius-lg;
padding: 1.5rem;
max-width: 320px;
width: 90%;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
animation: modalSlideIn $transition-fast ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modalHeader {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.modalIcon {
color: $red-500;
svg {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
}
}
.modalTitle {
font-size: 1rem;
font-weight: $semibold;
color: $gray-800;
}
.modalText {
font-size: 0.875rem;
line-height: 1.5;
color: $gray-600;
margin: 0 0 1.25rem 0;
}
.modalActions {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
.modalCancelButton {
padding: 0.5rem 1rem;
font-size: 0.8125rem;
font-weight: $medium;
color: $gray-600;
background-color: $gray-200;
border: none;
border-radius: $radius-md;
cursor: pointer;
transition: background-color $transition-fast;
&:hover {
background-color: $gray-300;
}
}
.modalConfirmButton {
padding: 0.5rem 1rem;
font-size: 0.8125rem;
font-weight: $medium;
color: $white;
background-color: $red-500;
border: none;
border-radius: $radius-md;
cursor: pointer;
transition: background-color $transition-fast;
&:hover {
background-color: darken($red-500, 10%);
}
}

525
source/Options/Form.tsx Normal file
View File

@@ -0,0 +1,525 @@
import {isNull, isUndefined} from '@abhijithvijayan/ts-utils';
import type {JSX} from 'react';
import {useState, useEffect, useRef, ChangeEvent} from 'react';
import clsx from 'clsx';
import {useExtensionSettings} from '../contexts/extension-settings-context';
import {
updateExtensionSettings,
clearExtensionSettings,
} from '../util/settings';
import {CHECK_API_KEY} from '../Background/constants';
import messageUtil from '../util/messageUtil';
import {isValidUrl} from '../util/link';
import {
SuccessfulApiKeyCheckProperties,
AuthRequestBodyProperties,
ApiErroredProperties,
ErrorStateProperties,
Kutt,
} from '../Background';
import Icon from '../components/Icon';
import styles from './Form.module.scss';
type OptionsFormValuesProperties = {
apikey: string;
history: boolean;
advanced: boolean;
host: string;
reuse: boolean;
};
type FormErrors = {
apikey?: string;
host?: string;
};
type FormValidity = {
apikey?: boolean;
host?: boolean;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onSave = (values: OptionsFormValuesProperties): Promise<any> =>
// should always return a Promise
updateExtensionSettings(values); // update local settings
function Form(): JSX.Element {
const extensionSettingsState = useExtensionSettings()[0];
const hostInputRef = useRef<HTMLInputElement>(null);
const [submitting, setSubmitting] = useState<boolean>(false);
const [showApiKey, setShowApiKey] = useState<boolean>(false);
const [showResetConfirm, setShowResetConfirm] = useState<boolean>(false);
const [errored, setErrored] = useState<ErrorStateProperties>({
error: null,
message: '',
});
const [formValues, setFormValues] = useState<OptionsFormValuesProperties>({
apikey: extensionSettingsState.apikey,
history: extensionSettingsState.history,
advanced: extensionSettingsState.advanced,
host:
(extensionSettingsState.advanced &&
extensionSettingsState.host.hostUrl) ||
'',
reuse: extensionSettingsState.reuse,
});
const [formErrors, setFormErrors] = useState<FormErrors>({});
const [formValidity, setFormValidity] = useState<FormValidity>({});
const isFormValid: boolean =
((isUndefined(formValidity.apikey) || formValidity.apikey) &&
formValues.apikey.trim().length === 40 &&
isUndefined(formErrors.apikey) &&
(((isUndefined(formValidity.host) || formValidity.host) &&
isUndefined(formErrors.host)) ||
!formValues.advanced)) ||
false;
// on component mount -> save `settings` object
useEffect(() => {
onSave({
...formValues,
...(formValues.advanced === false && {host: ''}),
});
}, [formValues]);
function handleApiKeyInputChange(apikey: string): void {
setFormValues((prev) => {
return {...prev, apikey};
});
// ToDo: Remove special symbols
if (!(apikey.trim().length > 0)) {
setFormErrors((prev) => {
return {...prev, apikey: 'API key missing'};
});
setFormValidity((prev) => {
return {...prev, apikey: false};
});
} else if (apikey && apikey.trim().length < 40) {
setFormErrors((prev) => {
return {...prev, apikey: 'API key must be 40 characters'};
});
setFormValidity((prev) => {
return {...prev, apikey: false};
});
} else if (apikey && apikey.trim().length > 40) {
setFormErrors((prev) => {
return {...prev, apikey: 'API key cannot exceed 40 characters'};
});
setFormValidity((prev) => {
return {...prev, apikey: false};
});
} else {
setFormErrors((prev) => {
const {apikey: _, ...rest} = prev;
return rest;
});
setFormValidity((prev) => {
return {...prev, apikey: true};
});
}
}
function handleHostUrlInputChange(host: string): void {
if (!formValues.advanced) {
setFormErrors((prev) => {
return {...prev, host: 'Enable Advanced Options first'};
});
setFormValidity((prev) => {
return {...prev, host: false};
});
return;
}
setFormValues((prev) => {
return {...prev, host};
});
if (!(host.trim().length > 0)) {
setFormErrors((prev) => {
return {...prev, host: 'Custom URL cannot be empty'};
});
setFormValidity((prev) => {
return {...prev, host: false};
});
return;
}
if (!isValidUrl(host.trim()) || host.trim().length < 10) {
setFormErrors((prev) => {
return {...prev, host: 'Please enter a valid url'};
});
setFormValidity((prev) => {
return {...prev, host: false};
});
} else {
setFormErrors((prev) => {
const {host: _, ...rest} = prev;
return rest;
});
setFormValidity((prev) => {
return {...prev, host: true};
});
}
}
async function handleApiKeyVerification(): Promise<void> {
setSubmitting(true);
// request API validation request
const apiKeyValidationBody: AuthRequestBodyProperties = {
apikey: formValues.apikey.trim(),
hostUrl:
(formValues.advanced &&
formValues.host.trim().length > 0 &&
formValues.host.trim()) ||
Kutt.hostUrl,
};
// API call
const response: SuccessfulApiKeyCheckProperties | ApiErroredProperties =
await messageUtil.send(CHECK_API_KEY, apiKeyValidationBody);
if (!response.error) {
// set top-level status
setErrored({error: false, message: 'Valid API Key'});
// Store user account information
const {domains, email} = response.data;
await updateExtensionSettings({user: {domains, email}});
} else {
// ---- errored ---- //
setErrored({error: true, message: response.message});
// Delete `user` field from settings
await updateExtensionSettings({user: null});
}
// enable validate button
setSubmitting(false);
setTimeout(() => {
// Reset status
setErrored({error: null, message: ''});
}, 3000);
}
async function handleResetSettings(): Promise<void> {
await clearExtensionSettings();
setShowResetConfirm(false);
// Reload the page to reflect cleared settings
window.location.reload();
}
return (
<>
<div className={styles.formSection}>
<div className={styles.inputGroup}>
<label htmlFor="apikey" className={styles.label}>
API Key
<span className={styles.labelLinkWrapper}>
<a
href={`${
(formValues.advanced && formValues.host) || Kutt.hostUrl
}/login`}
target="blank"
rel="nofollow noopener noreferrer"
className={styles.labelLink}
>
get one?
</a>
<span className={styles.tooltip}>
Get your API key from your Kutt account settings page
</span>
</span>
</label>
<div className={styles.inputWrapper}>
<div className={styles.inputIconWrapper}>
<Icon
className={styles.inputIcon}
onClick={(): void => setShowApiKey(!showApiKey)}
name={!showApiKey ? 'eye-closed' : 'eye'}
/>
</div>
<input
id="apikey"
name="apikey"
type={!showApiKey ? 'password' : 'text'}
value={formValues.apikey}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
handleApiKeyInputChange(e.target.value.trim());
}}
spellCheck="false"
className={clsx(
styles.input,
!isUndefined(formValidity.apikey) &&
!formValidity.apikey &&
styles.inputError
)}
/>
</div>
<span className={styles.errorText}>{formErrors.apikey}</span>
</div>
</div>
<div className={styles.validateSection}>
<button
type="button"
disabled={submitting || !isFormValid}
onClick={handleApiKeyVerification}
className={styles.validateButton}
>
<span className={styles.validateText}>Validate</span>
<Icon
name={
submitting
? 'spinner'
: (!isNull(errored.error) &&
((!errored.error && 'tick') || 'cross')) ||
'zap'
}
className={styles.validateIcon}
/>
</button>
{!isNull(errored.error) && (
<div
className={clsx(
styles.validationFeedback,
errored.error ? styles.error : styles.success
)}
>
<Icon
className={styles.feedbackIcon}
name={errored.error ? 'cross' : 'tick'}
/>
<span className={styles.feedbackMessage}>{errored.message}</span>
</div>
)}
</div>
<div className={styles.toggleSection}>
<label htmlFor="history" className={styles.toggleLabel}>
<span className={styles.toggleTextWithInfo}>
<span className={styles.toggleText}>Show Recent Links</span>
<span className={styles.infoIcon}>
<Icon name="info" />
<span className={styles.tooltip}>
Enables the History page to view your recent shortened links
</span>
</span>
</span>
<span className={styles.toggleWrapper}>
<span className={styles.toggleTrack} />
<span
className={clsx(
styles.toggleKnob,
formValues.history && styles.active
)}
>
<input
id="history"
name="history"
type="checkbox"
checked={formValues.history}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
setFormValues((prev) => {
return {...prev, history: e.target.checked};
});
}}
className={styles.toggleInput}
/>
</span>
</span>
</label>
<label htmlFor="reuse" className={styles.toggleLabel}>
<span className={styles.toggleTextWithInfo}>
<span className={styles.toggleText}>Reuse Existing URLs</span>
<span className={styles.infoIcon}>
<Icon name="info" />
<span className={styles.tooltip}>
Returns the existing short link if the same URL was shortened
before
</span>
</span>
</span>
<span className={styles.toggleWrapper}>
<span className={styles.toggleTrack} />
<span
className={clsx(
styles.toggleKnob,
formValues.reuse && styles.active
)}
>
<input
id="reuse"
name="reuse"
type="checkbox"
checked={formValues.reuse}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
setFormValues((prev) => {
return {...prev, reuse: e.target.checked};
});
}}
className={styles.toggleInput}
/>
</span>
</span>
</label>
<label htmlFor="advanced" className={styles.toggleLabel}>
<span className={styles.toggleTextWithInfo}>
<span className={styles.toggleText}>Show Advanced Options</span>
<span className={styles.infoIcon}>
<Icon name="info" />
<span className={styles.tooltip}>
Configure a custom self-hosted Kutt instance URL
</span>
</span>
</span>
<span className={styles.toggleWrapper}>
<span className={styles.toggleTrack} />
<span
className={clsx(
styles.toggleKnob,
formValues.advanced && styles.active
)}
>
<input
id="advanced"
name="advanced"
type="checkbox"
checked={formValues.advanced}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
setFormValues((prev) => {
return {...prev, advanced: e.target.checked};
});
if (e.target.checked) {
setTimeout(() => hostInputRef.current?.focus(), 350);
}
}}
className={styles.toggleInput}
/>
</span>
</span>
</label>
<div
className={clsx(
styles.advancedSection,
!formValues.advanced && styles.hidden
)}
>
<div className={styles.inputGroup}>
<label htmlFor="host" className={styles.label}>
<span className={styles.labelWithInfo}>
Custom Host
<span className={styles.infoIcon}>
<Icon name="info" />
<span className={styles.tooltip}>
URL of your self-hosted Kutt instance (e.g.,
https://kutt.example.com)
</span>
</span>
</span>
</label>
<div className={styles.inputWrapper}>
<input
ref={hostInputRef}
id="host"
name="host"
type="text"
value={formValues.host}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
handleHostUrlInputChange(e.target.value.trim());
}}
spellCheck="false"
className={clsx(
styles.input,
!isUndefined(formValidity.host) &&
!formValidity.host &&
styles.inputError
)}
/>
</div>
<span className={styles.errorText}>{formErrors.host}</span>
</div>
</div>
</div>
<div className={styles.resetSection}>
<button
type="button"
onClick={() => setShowResetConfirm(true)}
className={styles.resetButton}
>
Reset All Settings
</button>
<span className={styles.resetHint}>
This will clear all your settings and reload the extension
</span>
</div>
{showResetConfirm && (
<div
className={styles.modalOverlay}
onClick={() => setShowResetConfirm(false)}
onKeyDown={(e) => {
if (e.key === 'Escape') setShowResetConfirm(false);
}}
role="button"
tabIndex={0}
>
<div
className={styles.modal}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
role="button"
tabIndex={0}
>
<div className={styles.modalHeader}>
<Icon name="info" className={styles.modalIcon} />
<span className={styles.modalTitle}>Reset Settings?</span>
</div>
<p className={styles.modalText}>
This will permanently delete your API key and all preferences. You
will need to reconfigure the extension.
</p>
<div className={styles.modalActions}>
<button
type="button"
onClick={() => setShowResetConfirm(false)}
className={styles.modalCancelButton}
>
Cancel
</button>
<button
type="button"
onClick={handleResetSettings}
className={styles.modalConfirmButton}
>
Reset
</button>
</div>
</div>
</div>
)}
</>
);
}
export default Form;

View File

@@ -0,0 +1,45 @@
@use '../styles/variables' as *;
.header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-bottom: 1.5rem;
margin-bottom: 0.5rem;
border-bottom: 1px solid $gray-200;
}
.logoContainer {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
transition: opacity $transition-fast;
&:hover {
opacity: 0.8;
}
}
.logo {
width: 2.5rem;
height: 2.5rem;
}
.title {
font-weight: $semibold;
font-size: 1.75rem;
background: $primary-gradient;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0;
}
.subtitle {
margin-top: 0.5rem;
font-size: 0.875rem;
color: $gray-500;
font-weight: $regular;
}

39
source/Options/Header.tsx Normal file
View File

@@ -0,0 +1,39 @@
import type {JSX} from 'react';
import {memo} from 'react';
import {Kutt} from '../Background';
import styles from './Header.module.scss';
type Props = {
subtitle?: string;
hostUrl?: string;
};
function Header({
subtitle = 'Extension Settings',
hostUrl = Kutt.hostUrl,
}: Props): JSX.Element {
return (
<header className={styles.header}>
<a
href={hostUrl}
target="_blank"
rel="noopener noreferrer"
className={styles.logoContainer}
>
<img
className={styles.logo}
width="40"
height="40"
src="../assets/logo.png"
alt="logo"
/>
<h1 className={styles.title}>Kutt</h1>
</a>
<p className={styles.subtitle}>{subtitle}</p>
</header>
);
}
export default memo(Header);

View File

@@ -0,0 +1,30 @@
@use '../styles/variables' as *;
.optionsPage {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem 1.5rem;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
user-select: none;
}
.optionsContainer {
width: 100%;
max-width: 28rem;
padding: 2rem 2.5rem;
margin: 1rem;
background-color: $white;
height: max-content;
border-radius: $radius-lg;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
@media (min-width: 640px) {
padding: 2.5rem 3rem;
}
}
.loaderContainer {
height: 16rem;
}

108
source/Options/Options.tsx Normal file
View File

@@ -0,0 +1,108 @@
import type {JSX} from 'react';
import {useEffect, useState} from 'react';
import {getExtensionSettings} from '../util/settings';
import {
HostProperties,
useExtensionSettings,
ExtensionSettingsActionTypes,
} from '../contexts/extension-settings-context';
import {
useRequestStatus,
RequestStatusActionTypes,
} from '../contexts/request-status-context';
import {isValidUrl} from '../util/link';
import {Kutt} from '../Background';
import BodyWrapper from '../components/BodyWrapper';
import Loader from '../components/Loader';
import Header from './Header';
import Footer from './Footer';
import Form from './Form';
import styles from './Options.module.scss';
function Options(): JSX.Element {
const [, extensionSettingsDispatch] = useExtensionSettings();
const [requestStatusState, requestStatusDispatch] = useRequestStatus();
const [hostUrl, setHostUrl] = useState<string>(Kutt.hostUrl);
useEffect(() => {
async function getSavedSettings(): Promise<void> {
const {settings = {}} = await getExtensionSettings();
const advancedSettings: boolean =
(settings?.advanced as boolean) || false;
const defaultHost: HostProperties =
(advancedSettings &&
(settings?.host as string) &&
isValidUrl(settings.host as string) && {
hostDomain:
(settings.host as string)
.replace('http://', '')
.replace('https://', '')
.replace('www.', '')
.split(/[/?#]/)[0] || '', // extract domain
hostUrl: (settings.host as string).endsWith('/')
? (settings.host as string).slice(0, -1)
: (settings.host as string), // slice `/` at the end
}) ||
Kutt;
// inject existing keys (if field doesn't exist, use default)
// For history: default to true for new users, but respect existing user preference
const historyEnabled = Object.prototype.hasOwnProperty.call(
settings,
'history'
)
? (settings.history as boolean)
: true;
const defaultExtensionConfig = {
apikey: (settings?.apikey as string)?.trim() || '',
history: historyEnabled,
advanced:
defaultHost.hostUrl.trim() !== Kutt.hostUrl && advancedSettings, // disable `advanced` if customhost is not set
host: defaultHost,
reuse: (settings?.reuse as boolean) || false,
};
setHostUrl(defaultExtensionConfig.host.hostUrl);
extensionSettingsDispatch({
type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS,
payload: defaultExtensionConfig,
});
requestStatusDispatch({
type: RequestStatusActionTypes.SET_LOADING,
payload: false,
});
}
getSavedSettings();
}, [extensionSettingsDispatch, requestStatusDispatch]);
return (
<>
<BodyWrapper>
<div id="options" className={styles.optionsPage}>
<div className={styles.optionsContainer}>
<Header hostUrl={hostUrl} />
{!requestStatusState.loading ? (
<Form />
) : (
<div className={styles.loaderContainer}>
<Loader />
</div>
)}
<Footer />
</div>
</div>
</BodyWrapper>
</>
);
}
export default Options;

24
source/Options/index.tsx Normal file
View File

@@ -0,0 +1,24 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import {ExtensionSettingsProvider} from '../contexts/extension-settings-context';
import {RequestStatusProvider} from '../contexts/request-status-context';
import Options from './Options';
import '../styles/main.scss';
const container = document.getElementById('options-root');
if (!container) {
throw new Error('Could not find options-root container');
}
const root = createRoot(container);
root.render(
<StrictMode>
<ExtensionSettingsProvider>
<RequestStatusProvider>
<Options />
</RequestStatusProvider>
</ExtensionSettingsProvider>
</StrictMode>
);

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Options: Kutt</title>
</head>
<body>
<div id="options-root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,298 @@
@use '../styles/variables' as *;
.formContainer {
display: flex;
flex-direction: column;
width: 100%;
max-width: 24rem;
padding: 1rem;
margin: 0 auto;
background-color: $white;
user-select: none;
}
.formGroup {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
.formGroupRelative {
display: flex;
flex-direction: column;
margin-bottom: 0.75rem;
position: relative;
}
.label {
margin-bottom: 0.25rem;
font-size: 0.75rem;
letter-spacing: 0.025em;
color: $gray-600;
@media (min-width: 640px) {
font-size: 0.875rem;
}
}
.labelAbsolute {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
display: block;
font-size: 0.75rem;
letter-spacing: 0.025em;
color: $gray-600;
cursor: pointer;
}
.dropdown {
position: relative;
}
.dropdownTrigger {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
text-align: left;
background-color: $white;
border: 1.5px solid $gray-300;
border-radius: $radius-md;
cursor: pointer;
transition: border-color $transition-fast, box-shadow $transition-fast;
&:hover {
border-color: $gray-400;
}
&:focus {
outline: none;
border-color: $blue-500;
box-shadow: 0 0 0 3px rgba($blue-500, 0.1);
}
&.open {
border-color: $blue-500;
box-shadow: 0 0 0 3px rgba($blue-500, 0.1);
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
background-color: $gray-200;
}
}
.dropdownValue {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: $gray-500;
&.hasValue {
color: $gray-700;
}
}
.dropdownIcon {
display: flex;
align-items: center;
margin-left: 0.5rem;
color: $gray-400;
transition: transform $transition-fast, color $transition-fast;
&.open {
transform: rotate(180deg);
}
}
.dropdownMenu {
position: absolute;
top: calc(100% + 0.5rem);
left: 0;
right: 0;
z-index: 50;
max-height: 180px;
overflow-y: auto;
background-color: $white;
border-radius: $radius-md;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
animation: dropdownFade $transition-fast ease-out;
}
@keyframes dropdownFade {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dropdownItem {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
text-align: left;
color: $gray-700;
background-color: transparent;
border: none;
cursor: pointer;
transition: background-color $transition-fast;
&:first-child {
border-top-left-radius: $radius-md;
border-top-right-radius: $radius-md;
}
&:last-child {
border-bottom-left-radius: $radius-md;
border-bottom-right-radius: $radius-md;
}
&:hover {
background-color: $gray-200;
}
&.selected {
color: $gray-700;
font-weight: $medium;
}
}
.input {
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
background-color: $white;
border: 1.5px solid $gray-300;
border-radius: $radius-md;
margin-top: 1.2rem;
transition: border-color $transition-fast, box-shadow $transition-fast;
&::placeholder {
color: $gray-400;
}
&:hover {
border-color: $gray-400;
}
&:focus {
outline: none;
border-color: $blue-500;
box-shadow: 0 0 0 3px rgba($blue-500, 0.1);
}
@media (min-width: 640px) {
font-size: 1rem;
}
}
.inputError {
border-color: $red-500;
}
.errorText {
display: flex;
align-items: center;
margin-top: 0.25rem;
margin-left: 0.25rem;
font-size: 0.75rem;
font-weight: $medium;
letter-spacing: 0.025em;
color: $red-500;
}
.passwordWrapper {
position: relative;
}
.passwordToggle {
position: absolute;
top: 1.2rem;
right: 0;
bottom: 0;
display: flex;
align-items: center;
width: 2.5rem;
}
.passwordToggleIcon {
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 4px 0 0 4px;
cursor: pointer;
color: $gray-400;
transition: color $transition-fast;
&:hover {
color: $gray-600;
}
}
.submitButton {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
padding: 0.5rem 0.75rem;
margin-top: 0.5rem;
margin-bottom: 0.25rem;
font-size: 0.8125rem;
font-weight: $semibold;
text-align: center;
color: $white;
background: $primary-gradient;
border: none;
border-radius: $radius-md;
box-shadow: 0 4px 6px -1px rgba(126, 87, 194, 0.3);
min-height: 36px;
cursor: pointer;
transition: transform $transition-fast, box-shadow $transition-fast;
&:hover {
transform: translateY(-1px);
box-shadow: 0 6px 10px -1px rgba(126, 87, 194, 0.4);
}
&:active {
transform: translateY(0);
}
&:focus {
outline: none;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
}
.createIcon {
display: inline-flex;
padding: 0;
background-color: transparent;
svg {
stroke: currentColor;
stroke-width: 2;
transition: transform $transition-normal;
}
}

332
source/Popup/Form.tsx Normal file
View File

@@ -0,0 +1,332 @@
import type {JSX} from 'react';
import {useState, useRef, useEffect, type ChangeEvent} from 'react';
import {EMPTY_STRING, isEmpty, isNull, get} from '@abhijithvijayan/ts-utils';
import clsx from 'clsx';
import {useExtensionSettings} from '../contexts/extension-settings-context';
import {SHORTEN_URL} from '../Background/constants';
import messageUtil from '../util/messageUtil';
import {getCurrentTab} from '../util/tabs';
import {
RequestStatusActionTypes,
useRequestStatus,
} from '../contexts/request-status-context';
import {isValidUrl, removeProtocol} from '../util/link';
import {
SuccessfulShortenStatusProperties,
ShortUrlActionBodyProperties,
ApiErroredProperties,
ApiBodyProperties,
} from '../Background';
import Icon from '../components/Icon';
import styles from './Form.module.scss';
export enum CONSTANTS {
DefaultDomainId = 'default',
}
function Form(): JSX.Element {
const extensionSettingsState = useExtensionSettings()[0];
const requestStatusDispatch = useRequestStatus()[1];
const [showPassword, setShowPassword] = useState<boolean>(false);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [isDropdownOpen, setIsDropdownOpen] = useState<boolean>(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const {
domainOptions,
host: {hostDomain},
} = extensionSettingsState;
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent): void {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsDropdownOpen(false);
}
}
document.addEventListener('mousedown', handleClickOutside);
return (): void =>
document.removeEventListener('mousedown', handleClickOutside);
}, []);
const [formState, setFormState] = useState({
domain:
domainOptions
.find(({id}) => id === CONSTANTS.DefaultDomainId)
?.value?.trim() || EMPTY_STRING,
customurl: '',
password: '',
});
const [formErrors, setFormErrors] = useState<{
customurl?: string;
password?: string;
}>({});
const isFormValid: boolean =
!formErrors.customurl && !formErrors.password && true;
async function handleFormSubmit(): Promise<void> {
// enable loading screen
setIsSubmitting(true);
// Get target link to shorten
const tabs = await getCurrentTab();
const target: string | null = get(tabs, '[0].url', null);
const shouldSubmit: boolean = !isNull(target) && isValidUrl(target);
if (!shouldSubmit) {
setIsSubmitting(false);
requestStatusDispatch({
type: RequestStatusActionTypes.SET_REQUEST_STATUS,
payload: {
error: true,
message: 'Not a valid URL',
},
});
return;
}
const apiBody: ApiBodyProperties = {
apikey: extensionSettingsState.apikey,
target: target!,
...(formState.customurl.trim() !== EMPTY_STRING && {
customurl: formState.customurl.trim(),
}),
...(!isEmpty(formState.password) && {password: formState.password}),
reuse: extensionSettingsState.reuse,
...(formState.domain.trim() !== EMPTY_STRING && {
domain: formState.domain.trim(),
}),
};
const apiShortenUrlBody: ShortUrlActionBodyProperties = {
apiBody,
hostUrl: extensionSettingsState.host.hostUrl,
};
// shorten url in the background
const response: SuccessfulShortenStatusProperties | ApiErroredProperties =
await messageUtil.send(SHORTEN_URL, apiShortenUrlBody);
// disable spinner
setIsSubmitting(false);
if (!response.error) {
const {
data: {link},
} = response;
// show shortened url
requestStatusDispatch({
type: RequestStatusActionTypes.SET_REQUEST_STATUS,
payload: {
error: false,
message: link,
},
});
// reset form fields (keep domain selection)
setFormState((prev) => {
return {...prev, customurl: '', password: ''};
});
setFormErrors({});
} else {
// errored
requestStatusDispatch({
type: RequestStatusActionTypes.SET_REQUEST_STATUS,
payload: {
error: true,
message: response.message,
},
});
}
}
function handleCustomUrlInputChange(url: string): void {
setFormState((prev) => {
return {...prev, customurl: url};
});
if (url.length > 0 && url.length < 3) {
setFormErrors((prev) => {
return {
...prev,
customurl: 'Custom URL must be at-least 3 characters',
};
});
} else {
setFormErrors((prev) => {
return {...prev, customurl: undefined};
});
}
}
function handlePasswordInputChange(password: string): void {
setFormState((prev) => {
return {...prev, password};
});
if (password.length > 0 && password.length < 3) {
setFormErrors((prev) => {
return {
...prev,
password: 'Password must be at-least 3 characters',
};
});
} else {
setFormErrors((prev) => {
return {...prev, password: undefined};
});
}
}
return (
<div className={styles.formContainer}>
<div className={styles.formGroup}>
<label className={styles.label}>Domain</label>
<div className={styles.dropdown} ref={dropdownRef}>
<button
type="button"
className={clsx(
styles.dropdownTrigger,
isDropdownOpen && styles.open
)}
onClick={() => !isSubmitting && setIsDropdownOpen(!isDropdownOpen)}
disabled={isSubmitting}
>
<span
className={clsx(
styles.dropdownValue,
formState.domain && styles.hasValue
)}
>
{domainOptions.find(({value}) => value === formState.domain)
?.option ||
(formState.domain && removeProtocol(formState.domain)) ||
'Select domain'}
</span>
<Icon
name="chevron-down"
className={clsx(
styles.dropdownIcon,
isDropdownOpen && styles.open
)}
/>
</button>
{isDropdownOpen && (
<div className={styles.dropdownMenu}>
{domainOptions.filter(({disabled}) => !disabled).length > 0 ? (
domainOptions
.filter(({disabled}) => !disabled)
.map(({id, option, value}) => (
<button
type="button"
key={id}
className={clsx(
styles.dropdownItem,
formState.domain === value && styles.selected
)}
onClick={() => {
setFormState((prev) => {
return {...prev, domain: value};
});
setIsDropdownOpen(false);
}}
>
{option || removeProtocol(value)}
</button>
))
) : (
<span className={styles.dropdownItem}>
No domains available
</span>
)}
</div>
)}
</div>
</div>
<div className={styles.formGroupRelative}>
<label htmlFor="customurl" className={styles.labelAbsolute}>
<span>{hostDomain}/</span>
</label>
<input
id="customurl"
name="customurl"
type="text"
value={formState.customurl}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
handleCustomUrlInputChange(e.target.value.trim());
}}
disabled={isSubmitting}
spellCheck="false"
className={clsx(
styles.input,
formErrors.customurl && styles.inputError
)}
/>
<span className={styles.errorText}>{formErrors.customurl}</span>
</div>
<div className={styles.formGroupRelative}>
<label htmlFor="password" className={styles.labelAbsolute}>
<span>Password</span>
</label>
<div className={styles.passwordWrapper}>
<div className={styles.passwordToggle}>
<Icon
onClick={(): void => {
if (!isSubmitting) {
setShowPassword(!showPassword);
}
}}
name={!showPassword ? 'eye-closed' : 'eye'}
className={styles.passwordToggleIcon}
/>
</div>
<input
id="password"
name="password"
type={!showPassword ? 'password' : 'text'}
value={formState.password}
spellCheck="false"
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
handlePasswordInputChange(e.target.value);
}}
disabled={isSubmitting}
className={clsx(
styles.input,
formErrors.password && styles.inputError
)}
/>
</div>
<span className={styles.errorText}>{formErrors.password}</span>
</div>
<button
type="button"
disabled={!isFormValid || isSubmitting}
onClick={handleFormSubmit}
className={styles.submitButton}
>
{!isSubmitting ? (
<span>Create</span>
) : (
<Icon className={styles.createIcon} name="spinner" />
)}
</button>
</div>
);
}
export default Form;

View File

@@ -0,0 +1,95 @@
@use '../styles/variables' as *;
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
user-select: none;
}
.logoLink {
display: flex;
align-items: center;
text-decoration: none;
transition: opacity $transition-fast;
&:hover {
opacity: 0.8;
}
}
.logo {
width: 2rem;
height: 2rem;
}
.actions {
display: flex;
gap: 0.25rem;
}
.iconWrapper {
position: relative;
display: inline-flex;
&:hover .tooltip {
visibility: visible;
opacity: 1;
}
}
.styledIcon {
background-color: transparent;
box-shadow: none;
color: $gray-400;
transition: color $transition-fast;
&:hover {
color: $gray-600;
}
}
.tooltip {
visibility: hidden;
opacity: 0;
position: absolute;
top: calc(100% + 0.375rem);
left: 50%;
transform: translateX(-50%);
padding: 0.375rem 0.5rem;
font-size: 0.6875rem;
font-weight: $medium;
white-space: nowrap;
color: $white;
background-color: $gray-800;
border-radius: $radius-sm;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
transition: opacity $transition-fast, visibility $transition-fast;
z-index: 100;
pointer-events: none;
&::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 0.25rem;
border-style: solid;
border-color: transparent transparent $gray-800 transparent;
}
}
// Position the last tooltip (Settings) to the right to prevent overflow
.iconWrapper:last-child .tooltip {
left: auto;
right: 0;
transform: none;
&::before {
left: auto;
right: 0.5rem;
transform: none;
}
}

134
source/Popup/Header.tsx Normal file
View File

@@ -0,0 +1,134 @@
import {isNull, EMPTY_STRING} from '@abhijithvijayan/ts-utils';
import type {JSX} from 'react';
import {useState, useRef, useEffect} from 'react';
import clsx from 'clsx';
import {openExtOptionsPage, openHistoryPage} from '../util/tabs';
import {updateExtensionSettings} from '../util/settings';
import {CHECK_API_KEY} from '../Background/constants';
import {
ExtensionSettingsActionTypes,
useExtensionSettings,
} from '../contexts/extension-settings-context';
import messageUtil from '../util/messageUtil';
import {
SuccessfulApiKeyCheckProperties,
AuthRequestBodyProperties,
ApiErroredProperties,
ErrorStateProperties,
} from '../Background';
import Icon from '../components/Icon';
import styles from './Header.module.scss';
function Header(): JSX.Element {
const [extensionSettingsState, extensionSettingsDispatch] =
useExtensionSettings();
const [loading, setLoading] = useState<boolean>(false);
const [errored, setErrored] = useState<ErrorStateProperties>({
error: null,
message: EMPTY_STRING,
});
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Cleanup timer on unmount
useEffect(
() => (): void => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
},
[]
);
async function fetchUserDomains(): Promise<void> {
setLoading(true);
const apiKeyValidationBody: AuthRequestBodyProperties = {
apikey: extensionSettingsState.apikey,
hostUrl: extensionSettingsState.host.hostUrl,
};
const response: SuccessfulApiKeyCheckProperties | ApiErroredProperties =
await messageUtil.send(CHECK_API_KEY, apiKeyValidationBody);
setLoading(false);
if (!response.error) {
setErrored({error: false, message: 'Fetching domains successful'});
const {domains, email} = response.data;
await updateExtensionSettings({user: {domains, email}});
} else {
setErrored({error: true, message: response.message});
await updateExtensionSettings({user: null});
}
extensionSettingsDispatch({
type: ExtensionSettingsActionTypes.RELOAD_EXTENSION_SETTINGS,
payload: !extensionSettingsState.reload,
});
// Clear any existing timer before setting a new one
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
setErrored({error: null, message: EMPTY_STRING});
}, 1000);
}
const iconToShow = loading
? 'spinner'
: (!isNull(errored.error) && (!errored.error ? 'tick' : 'cross')) ||
'refresh';
return (
<header className={styles.header}>
<a
href={extensionSettingsState.host.hostUrl}
target="_blank"
rel="noopener noreferrer"
className={styles.logoLink}
>
<img
className={styles.logo}
width="32"
height="32"
src="../assets/logo.png"
alt="logo"
/>
</a>
<div className={styles.actions}>
<span className={styles.iconWrapper}>
<Icon
onClick={fetchUserDomains}
name={iconToShow}
className={clsx('icon', styles.styledIcon)}
/>
<span className={styles.tooltip}>Sync account</span>
</span>
{extensionSettingsState.history && (
<span className={styles.iconWrapper}>
<Icon
onClick={openHistoryPage}
name="clock"
className={clsx('icon', styles.styledIcon)}
/>
<span className={styles.tooltip}>History</span>
</span>
)}
<span className={styles.iconWrapper}>
<Icon
onClick={openExtOptionsPage}
name="settings"
className={clsx('icon', styles.styledIcon)}
/>
<span className={styles.tooltip}>Settings</span>
</span>
</div>
</header>
);
}
export default Header;

View File

@@ -0,0 +1,7 @@
@use '../styles/variables' as *;
.popup {
font-size: 1.125rem;
min-height: 350px;
min-width: 270px;
}

185
source/Popup/Popup.tsx Normal file
View File

@@ -0,0 +1,185 @@
import {isNull, EMPTY_STRING} from '@abhijithvijayan/ts-utils';
import type {JSX} from 'react';
import {useEffect} from 'react';
import {Kutt, UserSettingsResponseProperties} from '../Background';
import {openExtOptionsPage} from '../util/tabs';
import {isValidUrl} from '../util/link';
import {
ExtensionSettingsActionTypes,
DomainOptionsProperties,
useExtensionSettings,
HostProperties,
} from '../contexts/extension-settings-context';
import {
RequestStatusActionTypes,
useRequestStatus,
} from '../contexts/request-status-context';
import {getExtensionSettings} from '../util/settings';
import BodyWrapper from '../components/BodyWrapper';
import ResponseBody from './ResponseBody';
import PopupHeader from './Header';
import Loader from '../components/Loader';
import Form, {CONSTANTS} from './Form';
import styles from './Popup.module.scss';
function Popup(): JSX.Element {
const [extensionSettingsState, extensionSettingsDispatch] =
useExtensionSettings();
const [requestStatusState, requestStatusDispatch] = useRequestStatus();
const {reload: liveReloadFlag} = extensionSettingsState;
// re-renders on `liveReloadFlag` change
useEffect((): void => {
async function getUserSettings(): Promise<void> {
const {settings = {}} = await getExtensionSettings();
// No API Key set
if (
!Object.prototype.hasOwnProperty.call(settings, 'apikey') ||
(settings.apikey as string) === EMPTY_STRING
) {
requestStatusDispatch({
type: RequestStatusActionTypes.SET_REQUEST_STATUS,
payload: {
error: true,
message: 'Extension requires an API Key to work',
},
});
requestStatusDispatch({
type: RequestStatusActionTypes.SET_LOADING,
payload: false,
});
// Open options page
setTimeout(() => openExtOptionsPage(), 1300);
return;
}
let defaultHost: HostProperties = Kutt;
// If `advanced` field is true
if (
Object.prototype.hasOwnProperty.call(settings, 'advanced') &&
(settings.advanced as boolean)
) {
// If `host` field is set
if (
Object.prototype.hasOwnProperty.call(settings, 'host') &&
(settings.host as string)?.trim().length > 0 &&
isValidUrl(settings.host as string)
) {
defaultHost = {
hostDomain:
(settings.host as string)
.replace('http://', EMPTY_STRING)
.replace('https://', EMPTY_STRING)
.replace('www.', EMPTY_STRING)
.split(/[/?#]/)[0] || EMPTY_STRING,
hostUrl: (settings.host as string).endsWith('/')
? (settings.host as string).slice(0, -1)
: (settings.host as string),
};
}
}
// `history` field set - default to true for new users
let historyEnabled = true;
if (Object.prototype.hasOwnProperty.call(settings, 'history')) {
historyEnabled = settings.history as boolean;
}
// options menu
const defaultOptions: DomainOptionsProperties[] = [
{
id: EMPTY_STRING,
option: '-- Choose Domain --',
value: EMPTY_STRING,
disabled: true,
},
{
id: CONSTANTS.DefaultDomainId,
option: defaultHost.hostDomain,
value: defaultHost.hostDomain,
disabled: false,
},
];
// `user` & `apikey` fields exist on storage
if (
Object.prototype.hasOwnProperty.call(settings, 'user') &&
(settings.user as UserSettingsResponseProperties)
) {
const {user}: {user: UserSettingsResponseProperties} = settings;
let optionsList: DomainOptionsProperties[] = user.domains.map(
({id, address, homepage, banned}) => {
return {
id,
option: homepage,
value: address,
disabled: banned,
};
}
);
// merge to beginning of array
optionsList = defaultOptions.concat(optionsList);
// update domain list
extensionSettingsDispatch({
type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS,
payload: {
apikey: (settings.apikey as string)?.trim(),
domainOptions: optionsList,
host: defaultHost,
history: historyEnabled,
reuse: (settings.reuse as boolean) || false,
},
});
} else {
// no `user` but `apikey` exist on storage
extensionSettingsDispatch({
type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS,
payload: {
apikey: (settings.apikey as string)?.trim(),
domainOptions: defaultOptions,
host: defaultHost,
history: historyEnabled,
reuse: (settings.reuse as boolean) || false,
},
});
}
// stop loader
requestStatusDispatch({
type: RequestStatusActionTypes.SET_LOADING,
payload: false,
});
}
getUserSettings();
}, [liveReloadFlag, extensionSettingsDispatch, requestStatusDispatch]);
return (
<BodyWrapper>
<div id="popup" className={styles.popup}>
{!requestStatusState.loading ? (
<>
<PopupHeader />
{!isNull(requestStatusState.error) && <ResponseBody />}
<Form />
</>
) : (
<Loader />
)}
</div>
</BodyWrapper>
);
}
export default Popup;

View File

@@ -0,0 +1,62 @@
@use '../styles/variables' as *;
.popupBody {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem 1rem 0;
.icon {
cursor: pointer;
transition: opacity $transition-fast;
&:hover {
opacity: 0.7;
}
svg {
stroke: $green-500;
stroke-width: 2;
}
}
}
.qrIcon {
margin: 0;
margin-right: 0.4rem;
}
.copyIcon {
margin: 0;
margin-right: 0.75rem;
}
.link {
border-bottom: 1px dotted $stats-total-underline;
padding-bottom: 2px;
color: $gray-700;
min-width: 0;
margin: 0;
font-size: 1.5rem;
font-weight: $light;
cursor: pointer;
transition: color $transition-fast;
&:hover {
color: $blue-500;
}
}
.errorMessage {
padding-top: 0.25rem;
font-size: 0.9375rem;
font-weight: $medium;
color: $red-500;
}
.qrCodeContainer {
display: flex;
justify-content: center;
max-width: 100%;
padding: 1rem 0 0;
}

View File

@@ -0,0 +1,82 @@
import CopyToClipboard from 'react-copy-to-clipboard';
import type {JSX} from 'react';
import {useState, useEffect} from 'react';
import {QRCodeSVG} from 'qrcode.react';
import clsx from 'clsx';
import {useRequestStatus} from '../contexts/request-status-context';
import {removeProtocol} from '../util/link';
import Icon from '../components/Icon';
import styles from './ResponseBody.module.scss';
function ResponseBody(): JSX.Element {
const [{error, message}] = useRequestStatus();
const [copied, setCopied] = useState<boolean>(false);
const [QRView, setQRView] = useState<boolean>(false);
// reset copy message
useEffect(() => {
let timer: ReturnType<typeof setTimeout> | null = null;
timer = setTimeout(() => {
setCopied(false);
}, 1300);
return (): void => {
if (timer) {
clearTimeout(timer);
}
};
}, [copied]);
return (
<>
<div className={styles.popupBody}>
{!error ? (
<>
<Icon
className={clsx(styles.icon, styles.qrIcon)}
name="qrcode"
onClick={(): void => setQRView(!QRView)}
/>
{!copied ? (
<CopyToClipboard
text={message}
onCopy={(): void => setCopied(true)}
>
<Icon
className={clsx(styles.icon, styles.copyIcon)}
name="copy"
/>
</CopyToClipboard>
) : (
<Icon
className={clsx(styles.icon, styles.copyIcon)}
name="tick"
/>
)}
<CopyToClipboard
text={message}
onCopy={(): void => setCopied(true)}
>
<h1 className={styles.link}>{removeProtocol(message)}</h1>
</CopyToClipboard>
</>
) : (
<p className={styles.errorMessage}>{message}</p>
)}
</div>
{!error && QRView && (
<div className={styles.qrCodeContainer}>
<QRCodeSVG size={128} value={message} />
</div>
)}
</>
);
}
export default ResponseBody;

24
source/Popup/index.tsx Normal file
View File

@@ -0,0 +1,24 @@
import {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import {ExtensionSettingsProvider} from '../contexts/extension-settings-context';
import {RequestStatusProvider} from '../contexts/request-status-context';
import Popup from './Popup';
import '../styles/main.scss';
const container = document.getElementById('popup-root');
if (!container) {
throw new Error('Could not find popup-root container');
}
const root = createRoot(container);
root.render(
<StrictMode>
<ExtensionSettingsProvider>
<RequestStatusProvider>
<Popup />
</RequestStatusProvider>
</ExtensionSettingsProvider>
</StrictMode>
);

12
source/Popup/popup.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=500" />
<title>Kutt</title>
</head>
<body>
<div id="popup-root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,3 @@
.wrapper {
width: 100%;
}

View File

@@ -0,0 +1,12 @@
import type {JSX, ReactNode} from 'react';
import styles from './BodyWrapper.module.scss';
type WrapperProperties = {
children: ReactNode;
};
function BodyWrapper({children}: WrapperProperties): JSX.Element {
return <div className={styles.wrapper}>{children}</div>;
}
export default BodyWrapper;

View File

@@ -0,0 +1,18 @@
import React from 'react';
const ChevronDown: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M6 9l6 6 6-6" />
</svg>
);
export default React.memo(ChevronDown);

View File

@@ -0,0 +1,20 @@
import React from 'react';
const Clock: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="clock_svg__feather clock_svg__feather-clock"
>
<circle cx={12} cy={12} r={10} />
<path d="M12 6v6l4 2" />
</svg>
);
export default React.memo(Clock);

View File

@@ -0,0 +1,20 @@
import React from 'react';
const Copy: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="copy_svg__feather copy_svg__feather-copy"
>
<rect x={9} y={9} width={13} height={13} rx={2} ry={2} />
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
</svg>
);
export default React.memo(Copy);

View File

@@ -0,0 +1,19 @@
import React from 'react';
const Cross: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="x_svg__feather x_svg__feather-x"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
);
export default React.memo(Cross);

View File

@@ -0,0 +1,20 @@
import React from 'react';
const Eye: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="eye_svg__feather eye_svg__feather-eye"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx={12} cy={12} r={3} />
</svg>
);
export default React.memo(Eye);

View File

@@ -0,0 +1,19 @@
import React from 'react';
const EyeClosed: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className="eye-off_svg__feather eye-off_svg__feather-eye-off"
>
<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19m-6.72-1.07a3 3 0 11-4.24-4.24M1 1l22 22" />
</svg>
);
export default React.memo(EyeClosed);

View File

@@ -0,0 +1,55 @@
import React from 'react';
import ChevronDownIcon from './ChevronDown';
import StarYellowIcon from './StarYellow';
import EyeClosedIcon from './EyeClosed';
import StarWhiteIcon from './StarWhite';
import SettingsIcon from './Settings';
import RefreshIcon from './Refresh';
import SpinnerIcon from './Spinner';
import QRCodeIcon from './QRCode';
import CrossIcon from './Cross';
import ClockIcon from './Clock';
import CopyIcon from './Copy';
import TickIcon from './Tick';
import InfoIcon from './Info';
import ZapIcon from './Zap';
import EyeIcon from './Eye';
const icons = {
'chevron-down': ChevronDownIcon,
clock: ClockIcon,
copy: CopyIcon,
cross: CrossIcon,
eye: EyeIcon,
'eye-closed': EyeClosedIcon,
info: InfoIcon,
qrcode: QRCodeIcon,
refresh: RefreshIcon,
settings: SettingsIcon,
spinner: SpinnerIcon,
'star-yellow': StarYellowIcon,
'star-white': StarWhiteIcon,
tick: TickIcon,
zap: ZapIcon,
};
export type Icons = keyof typeof icons;
type Props = {
name: Icons;
title?: string;
stroke?: string;
fill?: string;
hoverFill?: string;
hoverStroke?: string;
strokeWidth?: string;
className?: string;
onClick?: () => void;
};
const Icon: React.FC<Props> = ({name, ...rest}) => (
<div {...rest}>{React.createElement(icons[name])}</div>
);
export default Icon;

View File

@@ -0,0 +1,20 @@
import React from 'react';
const Info: React.FC = () => (
<svg
width={14}
height={14}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
);
export default React.memo(Info);

View File

@@ -0,0 +1,19 @@
import React from 'react';
const QRCode: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="maximize_svg__feather maximize_svg__feather-maximize"
>
<path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" />
</svg>
);
export default React.memo(QRCode);

View File

@@ -0,0 +1,20 @@
import React from 'react';
const Refresh: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="refresh-ccw_svg__feather refresh-ccw_svg__feather-refresh-ccw"
>
<path d="M1 4v6h6M23 20v-6h-6" />
<path d="M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15" />
</svg>
);
export default React.memo(Refresh);

View File

@@ -0,0 +1,20 @@
import React from 'react';
const Settings: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="settings_svg__feather settings_svg__feather-settings"
>
<circle cx={12} cy={12} r={3} />
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z" />
</svg>
);
export default React.memo(Settings);

View File

@@ -0,0 +1,30 @@
import React from 'react';
const Spinner: React.FC = () => (
<>
<svg
id="spinner"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="none"
stroke="#b8b8b8"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="feather feather-loader"
viewBox="0 0 24 24"
>
<path d="M12 2L12 6" />
<path d="M12 18L12 22" />
<path d="M4.93 4.93L7.76 7.76" />
<path d="M16.24 16.24L19.07 19.07" />
<path d="M2 12L6 12" />
<path d="M18 12L22 12" />
<path d="M4.93 19.07L7.76 16.24" />
<path d="M16.24 7.76L19.07 4.93" />
</svg>
</>
);
export default React.memo(Spinner);

View File

@@ -0,0 +1,19 @@
import React from 'react';
const StarWhite: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="star_svg__feather star_svg__feather-star"
>
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
);
export default React.memo(StarWhite);

View File

@@ -0,0 +1,19 @@
import React from 'react';
const StarYellow: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="#ecc94b"
stroke="#ecc94b"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="star_svg__feather star_svg__feather-star"
>
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
);
export default React.memo(StarYellow);

View File

@@ -0,0 +1,19 @@
import React from 'react';
const Tick: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="check_svg__feather check_svg__feather-check"
>
<path d="M20 6L9 17l-5-5" />
</svg>
);
export default React.memo(Tick);

View File

@@ -0,0 +1,19 @@
import React from 'react';
const Zap: React.FC = () => (
<svg
width={16}
height={16}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="zap_svg__feather zap_svg__feather-zap"
>
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
</svg>
);
export default React.memo(Zap);

View File

@@ -0,0 +1 @@
export {default} from './Icon';

View File

@@ -0,0 +1,10 @@
.loader {
position: fixed;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@@ -0,0 +1,13 @@
import type {JSX} from 'react';
import Icon from './Icon';
import styles from './Loader.module.scss';
function Loader(): JSX.Element {
return (
<div className={styles.loader}>
<Icon name="spinner" />
</div>
);
}
export default Loader;

View File

@@ -0,0 +1,143 @@
import type {JSX} from 'react';
import {createContext, useReducer, useContext, type ReactNode} from 'react';
import {Kutt} from '../Background';
export enum ExtensionSettingsActionTypes {
HYDRATE_EXTENSION_SETTINGS = 'set-extension-settings',
RELOAD_EXTENSION_SETTINGS = 'reload-extension-settings',
}
export type HostProperties = {
hostDomain: string;
hostUrl: string;
};
export type DomainOptionsProperties = {
option: string;
value: string;
id: string;
disabled?: boolean;
};
type HYDRATE_EXTENSION_SETTINGS = {
type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS;
payload:
| {
apikey: string;
domainOptions: DomainOptionsProperties[];
host: HostProperties;
history: boolean;
reuse: boolean;
}
| {
apikey: string;
host: HostProperties;
history: boolean;
advanced: boolean;
reuse: boolean;
};
};
type RELOAD_EXTENSION_SETTINGS = {
type: ExtensionSettingsActionTypes.RELOAD_EXTENSION_SETTINGS;
payload: boolean;
};
type Action = HYDRATE_EXTENSION_SETTINGS | RELOAD_EXTENSION_SETTINGS;
type InitialValues = {
apikey: string;
domainOptions: DomainOptionsProperties[];
host: HostProperties;
reload: boolean;
history: boolean;
advanced: boolean;
reuse: boolean;
};
const initialValues: InitialValues = {
apikey: '',
domainOptions: [],
host: Kutt,
reload: false,
history: true,
advanced: false,
reuse: false,
};
type State = InitialValues;
type Dispatch = (action: Action) => void;
const ExtensionSettingsStateContext = createContext<State | undefined>(
undefined
);
const ExtensionSettingsDispatchContext = createContext<Dispatch | undefined>(
undefined
);
function extensionSettingsReducer(state: State, action: Action): State {
switch (action.type) {
case ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS: {
return {...state, ...action.payload};
}
case ExtensionSettingsActionTypes.RELOAD_EXTENSION_SETTINGS: {
return {...state, reload: action.payload};
}
default:
return state;
}
}
function useExtensionSettingsState(): State {
const context = useContext(ExtensionSettingsStateContext);
if (context === undefined) {
throw new Error(
'useExtensionSettingsState must be used within a ExtensionSettingsProvider'
);
}
return context;
}
function useExtensionSettingsDispatch(): Dispatch {
const context = useContext(ExtensionSettingsDispatchContext);
if (context === undefined) {
throw new Error(
'useExtensionSettingsDispatch must be used within a ExtensionSettingsProvider'
);
}
return context;
}
function useExtensionSettings(): [State, Dispatch] {
// To access const [state, dispatch] = useExtensionSettings()
return [useExtensionSettingsState(), useExtensionSettingsDispatch()];
}
type ExtensionSettingsProviderProps = {
children: ReactNode;
};
function ExtensionSettingsProvider({
children,
}: ExtensionSettingsProviderProps): JSX.Element {
const [state, dispatch] = useReducer(extensionSettingsReducer, initialValues);
return (
<>
<ExtensionSettingsStateContext.Provider value={state}>
<ExtensionSettingsDispatchContext.Provider value={dispatch}>
{children}
</ExtensionSettingsDispatchContext.Provider>
</ExtensionSettingsStateContext.Provider>
</>
);
}
export {ExtensionSettingsProvider, useExtensionSettings};

View File

@@ -0,0 +1,108 @@
import type {JSX} from 'react';
import {createContext, useReducer, useContext, type ReactNode} from 'react';
export enum RequestStatusActionTypes {
SET_REQUEST_STATUS = 'set-request-status',
SET_LOADING = 'set-loading',
}
type SET_REQUEST_STATUS = {
type: RequestStatusActionTypes.SET_REQUEST_STATUS;
payload: {
error: boolean;
message: string;
};
};
type SET_LOADING = {
type: RequestStatusActionTypes.SET_LOADING;
payload: boolean;
};
type Action = SET_REQUEST_STATUS | SET_LOADING;
type InitialValues = {
loading: boolean;
error: boolean | null;
message: string;
};
const initialValues: InitialValues = {
loading: true,
error: null,
message: '',
};
type State = InitialValues;
type Dispatch = (action: Action) => void;
const RequestStatusStateContext = createContext<State | undefined>(undefined);
const RequestStatusDispatchContext = createContext<Dispatch | undefined>(
undefined
);
function requestStatusReducer(state: State, action: Action): State {
switch (action.type) {
case RequestStatusActionTypes.SET_REQUEST_STATUS: {
return {...state, ...action.payload};
}
case RequestStatusActionTypes.SET_LOADING: {
return {...state, loading: action.payload};
}
default:
return state;
}
}
function useRequestStatusState(): State {
const context = useContext(RequestStatusStateContext);
if (context === undefined) {
throw new Error(
'useRequestStatusState must be used within a RequestStatusProvider'
);
}
return context;
}
function useRequestStatusDispatch(): Dispatch {
const context = useContext(RequestStatusDispatchContext);
if (context === undefined) {
throw new Error(
'useRequestStatusDispatch must be used within a RequestStatusProvider'
);
}
return context;
}
function useRequestStatus(): [State, Dispatch] {
// To access const [state, dispatch] = useRequestStatus()
return [useRequestStatusState(), useRequestStatusDispatch()];
}
type RequestStatusProviderProps = {
children: ReactNode;
};
function RequestStatusProvider({
children,
}: RequestStatusProviderProps): JSX.Element {
const [state, dispatch] = useReducer(requestStatusReducer, initialValues);
return (
<>
<RequestStatusStateContext.Provider value={state}>
<RequestStatusDispatchContext.Provider value={dispatch}>
{children}
</RequestStatusDispatchContext.Provider>
</RequestStatusStateContext.Provider>
</>
);
}
export {RequestStatusProvider, useRequestStatus};

View File

@@ -0,0 +1,115 @@
import type {JSX} from 'react';
import {createContext, useContext, useReducer, type ReactNode} from 'react';
import {UserShortenedLinkStats} from '../Background';
export enum ShortenedLinksActionTypes {
HYDRATE_SHORTENED_LINKS = 'hydrate-shortened-links',
SET_CURRENT_SELECTED = 'set-current-selected',
}
type HYDRATE_SHORTENED_LINKS = {
type: ShortenedLinksActionTypes.HYDRATE_SHORTENED_LINKS;
payload: {
items: UserShortenedLinkStats[];
total: number;
};
};
type SET_CURRENT_SELECTED = {
type: ShortenedLinksActionTypes.SET_CURRENT_SELECTED;
payload: string;
};
type Action = HYDRATE_SHORTENED_LINKS | SET_CURRENT_SELECTED;
type InitialValues = {
items: UserShortenedLinkStats[];
total: number;
selected: UserShortenedLinkStats | null;
};
const initialValues: InitialValues = {
items: [],
total: 0,
selected: null,
};
type State = InitialValues;
type Dispatch = (action: Action) => void;
const ShortenedLinksStateContext = createContext<State | undefined>(undefined);
const ShortenedLinksDispatchContext = createContext<Dispatch | undefined>(
undefined
);
const shortenedLinksReducer = (state: State, action: Action): State => {
switch (action.type) {
case ShortenedLinksActionTypes.HYDRATE_SHORTENED_LINKS: {
return {
...state,
...action.payload,
};
}
case ShortenedLinksActionTypes.SET_CURRENT_SELECTED: {
const selected: null | UserShortenedLinkStats =
state.items.filter((item) => item.id === action.payload)[0] || null;
return {...state, selected};
}
default:
return state;
}
};
function useShortenedLinksContextState(): State {
const context = useContext(ShortenedLinksStateContext);
if (context === undefined) {
throw new Error(
'useShortenedLinksContextState must be used within a ShortenedLinksProvider'
);
}
return context;
}
function useShortenedLinksContextDispatch(): Dispatch {
const context = useContext(ShortenedLinksDispatchContext);
if (context === undefined) {
throw new Error(
'useShortenedLinksContextDispatch must be used within a ShortenedLinksProvider'
);
}
return context;
}
function useShortenedLinks(): [State, Dispatch] {
return [useShortenedLinksContextState(), useShortenedLinksContextDispatch()];
}
type ShortenedLinksProviderProps = {
children: ReactNode;
};
function ShortenedLinksProvider({
children,
}: ShortenedLinksProviderProps): JSX.Element {
const [state, dispatch] = useReducer(shortenedLinksReducer, initialValues);
return (
<>
<ShortenedLinksStateContext.Provider value={state}>
<ShortenedLinksDispatchContext.Provider value={dispatch}>
{children}
</ShortenedLinksDispatchContext.Provider>
</ShortenedLinksStateContext.Provider>
</>
);
}
export {useShortenedLinks, ShortenedLinksProvider};

12
source/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,12 @@
declare const __DEV__: boolean;
declare const __TARGET_BROWSER__: 'chrome' | 'firefox';
declare module '*.module.scss' {
const classes: {readonly [key: string]: string};
export default classes;
}
declare module '*.scss' {
const content: string;
export default content;
}

52
source/manifest.json Normal file
View File

@@ -0,0 +1,52 @@
{
"manifest_version": 3,
"name": "Kutt",
"version": "0.0.0",
"short_name": "Kutt",
"description": "Shorten long URLs with just one click.",
"icons": {
"16": "assets/icons/favicon-16.png",
"32": "assets/icons/favicon-32.png",
"48": "assets/icons/favicon-48.png",
"128": "assets/icons/favicon-128.png"
},
"homepage_url": "https://github.com/thedevs-network/kutt-extension.git",
"__firefox__browser_specific_settings": {
"gecko": {
"id": "support@kutt.it",
"strict_min_version": "112.0",
"data_collection_permissions": {
"required": ["websiteActivity"],
"optional": []
}
}
},
"__chrome|firefox__author": "abhijithvijayan",
"action": {
"default_popup": "Popup/popup.html",
"default_icon": {
"16": "assets/icons/favicon-16.png",
"32": "assets/icons/favicon-32.png",
"48": "assets/icons/favicon-48.png",
"128": "assets/icons/favicon-128.png"
},
"default_title": "Shorten this URL"
},
"background": {
"__chrome__service_worker": "assets/js/background.bundle.js",
"__chrome__type": "module",
"__firefox__scripts": ["assets/js/background.bundle.js"],
"__firefox__type": "module"
},
"__chrome__minimum_chrome_version": "88",
"permissions": ["activeTab", "storage"],
"host_permissions": ["http://*/*", "https://*/*"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self';"
},
"__chrome__options_page": "Options/options.html",
"options_ui": {
"page": "Options/options.html",
"open_in_tab": true
}
}

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 984 B

After

Width:  |  Height:  |  Size: 984 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,25 @@
@use 'variables' as *;
.icon {
width: 26px;
height: 26px;
background-color: rgb(235, 255, 243);
align-items: center;
justify-content: center;
display: flex;
position: relative;
cursor: pointer;
box-shadow: rgba(138, 158, 168, 0.12) 0px 2px 1px;
padding: 4px;
outline: none;
transition: transform 0.4s ease-out 0s;
border-radius: 100%;
svg {
transition: all 0.2s ease-out 0s;
}
}
.d-none {
display: none !important;
}

67
source/styles/_reset.scss Normal file
View File

@@ -0,0 +1,67 @@
// CSS reset
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
height: auto;
-webkit-text-size-adjust: 100%;
}
body {
min-height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.5;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
a {
color: inherit;
text-decoration: none;
}
button {
background: none;
border: none;
cursor: pointer;
}
ul,
ol {
list-style: none;
}
// Spinner animation (used by Icon component)
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
#spinner {
animation: spin 1s linear infinite;
}

View File

@@ -0,0 +1,48 @@
// Colors
$black: #111111;
$light-black: #0f0f0f;
$grey-white: #f3f3f3;
$white: #ffffff;
// Tailwind-like colors
$gray-200: #e5e7eb;
$gray-300: #d1d5db;
$gray-400: #9ca3af;
$gray-500: #6b7280;
$gray-600: #4b5563;
$gray-700: #374151;
$gray-800: #1f2937;
$gray-900: #111827;
$red-500: #ef4444;
$green-500: #22c55e;
$green-900: #14532d;
$blue-500: #3b82f6;
$indigo-400: #818cf8;
$purple-600: #9333ea;
$yellow-500: #eab308;
// Custom colors
$copy-icon-bg: hsl(144, 100%, 96%);
$stats-total-underline: hsl(200, 35%, 65%);
$primary-gradient: linear-gradient(to right, rgb(126, 87, 194), rgb(98, 0, 234));
// Font weights
$thin: 100;
$exlight: 200;
$light: 300;
$regular: 400;
$medium: 500;
$semibold: 600;
$bold: 700;
$exbold: 800;
$exblack: 900;
// Transitions
$transition-fast: 200ms ease-out;
$transition-normal: 300ms ease-in-out;
// Border radius
$radius-sm: 4px;
$radius-md: 8px;
$radius-lg: 12px;
$radius-full: 9999px;

3
source/styles/main.scss Normal file
View File

@@ -0,0 +1,3 @@
@use 'reset';
@use 'variables';
@use 'components';

47
source/util/browser.ts Normal file
View File

@@ -0,0 +1,47 @@
import {EMPTY_STRING} from '@abhijithvijayan/ts-utils';
// Custom fork of https://github.com/DamonOehlman/detect-browser/blob/master/src/index.ts
type Browser = 'edge-chromium' | 'chrome' | 'firefox' | 'opera';
type UserAgentRule = [Browser, RegExp];
type UserAgentMatch = [Browser, RegExpExecArray] | false;
const userAgentRules: UserAgentRule[] = [
['edge-chromium', /Edg\/([0-9.]+)/],
['chrome', /(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9.]+)(:?\s|$)/],
['firefox', /Firefox\/([0-9.]+)(?:\s|$)/],
['opera', /Opera\/([0-9.]+)(?:\s|$)/],
['opera', /OPR\/([0-9.]+)(:?\s|$)/],
];
function matchUserAgent(ua: string): UserAgentMatch {
return (
ua !== EMPTY_STRING &&
userAgentRules.reduce<UserAgentMatch>(
(matched: UserAgentMatch, [browser, regex]) => {
if (matched) {
return matched;
}
const uaMatch = regex.exec(ua);
return !!uaMatch && [browser, uaMatch];
},
false
)
);
}
export function detectBrowser(): Browser | null {
const matchedRule = matchUserAgent(navigator.userAgent);
if (!matchedRule) {
return null;
}
const [name, match] = matchedRule;
let versionParts = match[1] && match[1].split(/[._]/).slice(0, 3);
if (!versionParts) {
versionParts = [];
}
return name;
}

14
source/util/link.ts Normal file
View File

@@ -0,0 +1,14 @@
import {IPV4_REGEX} from '@abhijithvijayan/ts-utils';
export const removeProtocol = (link: string): string =>
link.replace(/^https?:\/\//i, '').replace(/^www\./i, '');
export function isValidUrl(url: string): boolean {
// https://regex101.com/r/BzoIRR/1
const ipRegex = IPV4_REGEX.toString().slice(2, -2);
const re = new RegExp(
`^(http[s]?:\\/\\/)(www\\.){0,1}(([a-zA-Z0-9.-]+\\.[a-zA-Z]{2,5}[.]{0,1})|(${ipRegex}))`
);
return re.test(url);
}

View File

@@ -0,0 +1,14 @@
import browser from 'webextension-polyfill';
const messageUtil = {
send(name: string, params?: unknown): Promise<any> {
const data = {
action: name,
params,
};
return browser.runtime.sendMessage(data);
},
};
export default messageUtil;

38
source/util/settings.ts Normal file
View File

@@ -0,0 +1,38 @@
import browser from 'webextension-polyfill';
import {DomainEntryProperties} from '../Background';
// Core Extensions settings props
export type ExtensionSettingsProperties = {
apikey: string;
history: boolean;
user?: {
email?: string;
domains?: DomainEntryProperties[];
} | null;
};
// update extension settings in browser storage
export function saveExtensionSettings(settings: any): Promise<void> {
return browser.storage.local.set({
settings,
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getExtensionSettings(): Promise<{[s: string]: any}> {
return browser.storage.local.get('settings');
}
export async function updateExtensionSettings(newFields?: {
[s: string]: any;
}): Promise<void> {
const {settings = {}} = await getExtensionSettings();
return saveExtensionSettings({...settings, ...newFields});
}
// Clear all extension settings
export function clearExtensionSettings(): Promise<void> {
return browser.storage.local.clear();
}

20
source/util/tabs.ts Normal file
View File

@@ -0,0 +1,20 @@
import browser from 'webextension-polyfill';
import type {Tabs} from 'webextension-polyfill';
export function openExtOptionsPage(): Promise<void> {
return browser.runtime.openOptionsPage();
}
export function openHistoryPage(): Promise<Tabs.Tab> {
return browser.tabs.create({
active: true,
url: '/History/history.html',
});
}
export function getCurrentTab(): Promise<Tabs.Tab[]> {
return browser.tabs.query({
active: true,
lastFocusedWindow: true,
});
}

View File

@@ -1 +0,0 @@
<svg aria-hidden="true" data-prefix="fas" data-icon="location-arrow" class="svg-inline--fa fa-location-arrow fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M444.52 3.52L28.74 195.42c-47.97 22.39-31.98 92.75 19.19 92.75h175.91v175.91c0 51.17 70.36 67.17 92.75 19.19l191.9-415.78c15.99-38.39-25.59-79.97-63.97-63.97z"></path></svg>

Before

Width:  |  Height:  |  Size: 388 B

View File

@@ -1 +0,0 @@
<svg aria-hidden="true" data-prefix="far" data-icon="copy" class="svg-inline--fa fa-copy fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM266 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 0 1-6 6zm128-96H182a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h106v88c0 13.255 10.745 24 24 24h88v202a6 6 0 0 1-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 0 1 1.757 4.243V112z"></path></svg>

Before

Width:  |  Height:  |  Size: 718 B

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M896 128q209 0 385.5 103t279.5 279.5 103 385.5q0 251-146.5 451.5t-378.5 277.5q-27 5-40-7t-13-30q0-3 .5-76.5t.5-134.5q0-97-52-142 57-6 102.5-18t94-39 81-66.5 53-105 20.5-150.5q0-119-79-206 37-91-8-204-28-9-81 11t-92 44l-38 24q-93-26-192-26t-192 26q-16-11-42.5-27t-83.5-38.5-85-13.5q-45 113-8 204-79 87-79 206 0 85 20.5 150t52.5 105 80.5 67 94 39 102.5 18q-39 36-49 103-21 10-45 15t-57 5-65.5-21.5-55.5-62.5q-19-32-48.5-52t-49.5-24l-20-3q-21 0-29 4.5t-5 11.5 9 14 13 12l7 5q22 10 43.5 38t31.5 51l10 23q13 38 44 61.5t67 30 69.5 7 55.5-3.5l23-4q0 38 .5 88.5t.5 54.5q0 18-13 30t-40 7q-232-77-378.5-277.5t-146.5-451.5q0-209 103-385.5t279.5-279.5 385.5-103zm-477 1103q3-7-7-12-10-3-13 2-3 7 7 12 9 6 13-2zm31 34q7-5-2-16-10-9-16-3-7 5 2 16 10 10 16 3zm30 45q9-7 0-19-8-13-17-6-9 5 0 18t17 7zm42 42q8-8-4-19-12-12-20-3-9 8 4 19 12 12 20 3zm57 25q3-11-13-16-15-4-19 7t13 15q15 6 19-6zm63 5q0-13-17-11-16 0-16 11 0 13 17 11 16 0 16-11zm58-10q-2-11-18-9-16 3-14 15t18 8 14-14z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1 +0,0 @@
<svg aria-hidden="true" data-prefix="fas" data-icon="qrcode" class="svg-inline--fa fa-qrcode fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M0 224h192V32H0v192zM64 96h64v64H64V96zm192-64v192h192V32H256zm128 128h-64V96h64v64zM0 480h192V288H0v192zm64-128h64v64H64v-64zm352-64h32v128h-96v-32h-32v96h-64V288h96v32h64v-32zm0 160h32v32h-32v-32zm-64 0h32v32h-32v-32z"></path></svg>

Before

Width:  |  Height:  |  Size: 433 B

View File

@@ -1 +0,0 @@
<svg aria-hidden="true" data-prefix="fas" data-icon="cog" class="svg-inline--fa fa-cog fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M444.788 291.1l42.616 24.599c4.867 2.809 7.126 8.618 5.459 13.985-11.07 35.642-29.97 67.842-54.689 94.586a12.016 12.016 0 0 1-14.832 2.254l-42.584-24.595a191.577 191.577 0 0 1-60.759 35.13v49.182a12.01 12.01 0 0 1-9.377 11.718c-34.956 7.85-72.499 8.256-109.219.007-5.49-1.233-9.403-6.096-9.403-11.723v-49.184a191.555 191.555 0 0 1-60.759-35.13l-42.584 24.595a12.016 12.016 0 0 1-14.832-2.254c-24.718-26.744-43.619-58.944-54.689-94.586-1.667-5.366.592-11.175 5.459-13.985L67.212 291.1a193.48 193.48 0 0 1 0-70.199l-42.616-24.599c-4.867-2.809-7.126-8.618-5.459-13.985 11.07-35.642 29.97-67.842 54.689-94.586a12.016 12.016 0 0 1 14.832-2.254l42.584 24.595a191.577 191.577 0 0 1 60.759-35.13V25.759a12.01 12.01 0 0 1 9.377-11.718c34.956-7.85 72.499-8.256 109.219-.007 5.49 1.233 9.403 6.096 9.403 11.723v49.184a191.555 191.555 0 0 1 60.759 35.13l42.584-24.595a12.016 12.016 0 0 1 14.832 2.254c24.718 26.744 43.619 58.944 54.689 94.586 1.667 5.366-.592 11.175-5.459 13.985L444.788 220.9a193.485 193.485 0 0 1 0 70.2zM336 256c0-44.112-35.888-80-80-80s-80 35.888-80 80 35.888 80 80 80 80-35.888 80-80z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,28 +0,0 @@
{
"name": "kuttUrl - Shorten URLs",
"version": "0.2.0",
"description": "URL Shortener",
"background": {
"scripts": ["js/background.js"],
"persistent": false
},
"permissions": ["tabs", "storage", "clipboardWrite", "clipboardRead"],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"manifest_version": 2,
"browser_action": {
"default_popup": "popup.html",
"default_icon": {
"16": "assets/favicon-16.png",
"32": "assets/favicon-32.png",
"48": "assets/favicon-48.png",
"128": "assets/favicon-128.png"
}
},
"options_page": "options.html",
"icons": {
"16": "assets/favicon-16.png",
"32": "assets/favicon-32.png",
"48": "assets/favicon-48.png",
"128": "assets/favicon-128.png"
}
}

View File

@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Options : kuttUrl</title>
<link rel="stylesheet" href="css/options.css" />
</head>
<body>
<section id="options">
<div class="container options__content--holder">
<div class="head__content--holder">
<img class="head__content--logo" src="assets/logo.png" />
<h2 class="head__content--title">kuttUrl</h2>
</div>
<div class="form__content--holder">
<form class="form__content">
<label class="api__key--label">API Key:</label>
<input class="api__key--holder" id="api__key--value" type="text" />
<br>
<label class="saved__alert v-none">Saved!!</label>
<button class="button__submit" id="button__submit" type="button">Save</button>
</form>
</div>
</div>
</section>
<script src="js/options.js"></script>
</body>
</html>

View File

@@ -1,53 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>kuttUrl</title>
<link rel="stylesheet" href="css/popup.css" />
</head>
<body>
<section id="home">
<div class="container">
<nav class="navbar">
<ul class="navbar__main">
<li class="logo__content--holder">
<img class="main__logo" src="assets/logo.png">
</li>
<li class="github__content-holder">
<a href="https://github.com/abhijithvijayan/kuttUrl-Chrome/" target="_blank" rel="noopener" title="GitHub">
<img class="github__logo" src="assets/github.svg"></a>
</li>
<li class="options__content-holder" id="options__content-holder" title="Options">
<a href="options.html" target="_blank" rel="noopener">
<img class="settings__logo" src="assets/settings.svg"></a>
</li>
</ul>
</nav>
<div class="content__holder">
<div class="url__content--holder">
<h4 id="url__content-inner">Shortening...</h4>
</div>
<ul class="buttons__content--holder d-none">
<li class="copy__content--holder">
<img id="button__copy" src="assets/copy.svg" alt="copy" title="Copy"/>
</li>
<li class="details__content--holder">
<img id="button__details" src="assets/arrow.svg" alt="copy" title="Details" />
</li>
<li class="qrbtn__content--holder">
<img id="button__qrcode" src="assets/qrcode.svg" alt="copy" title="QR code" />
</li>
</ul>
<p class="copy__alert d-none">Copied!!!</p>
<div class="qrcode__content--holder d-none">
<img id="qr_code" src="#" alt="QRCode" />
</div>
</div>
</div>
</section>
<script src="js/popup.js"></script>
</body>
</html>

View File

@@ -1,38 +0,0 @@
import axios from "axios";
// Shorten url
async function getShortURL(API_key, URLtoShorten) {
let shortLink;
const api_url = 'https://cors-anywhere.herokuapp.com/https://kutt.it/api/url/submit';
try {
const rawData = await axios({
method: "POST",
url: api_url,
headers: {
'X-API-Key': API_key
},
data: { target: URLtoShorten }
});
shortLink = rawData.data.shortUrl;
} catch (error) {
console.log(error);
}
// returns the promise
return shortLink;
};
// Calling function
chrome.runtime.onMessage.addListener(
// receive the message
(request, sender, sendResponse) => {
if(request.msg == "start") {
let shortLink;
// consume the promise
getShortURL(request.API_key, request.pageUrl).then((data) => {
shortLink = data;
sendResponse({ shortUrl: `${shortLink}` });
});
return true;
}
}
);

View File

@@ -1,32 +0,0 @@
// update UI - API Key on options page load
document.addEventListener('DOMContentLoaded', () => {
// replace the input value with current value on load
chrome.storage.local.get(['key'], function(result) {
// to string
let API_KEY = `${result.key}`;
if (API_KEY === 'undefined') {
document.getElementById('api__key--value').value = '';
} else {
document.getElementById('api__key--value').value = API_KEY;
}
});
});
// Store new API Key on save click
document.getElementById('button__submit').addEventListener('click', () => {
let API_KEY = document.getElementById('api__key--value').value;
// store value locally
chrome.storage.local.set({key: API_KEY}, function() {
console.log('Value is set to ' + API_KEY);
});
});
// Saved Alert
document.getElementById('button__submit').addEventListener('click', () => {
let element = document.querySelector('.saved__alert');
element.classList.toggle('v-none');
setTimeout(() => {
element.classList.toggle('v-none');
}, 1300);
});

View File

@@ -1,95 +0,0 @@
let shortUrl;
document.addEventListener('DOMContentLoaded', () => {
// 1. Pass the message and receive response
chrome.tabs.query({'active': true, 'lastFocusedWindow': true}, (tabs) => {
let longUrl, start, qrcode__src = 'https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=';
let API_key;
longUrl = tabs[0].url;
start = longUrl.substr(0, 6);
// i) Get api key from options page
chrome.storage.local.get(['key'], function(result) {
API_key = result.key;
if(start !== 'chrome' && API_key !== '' && API_key !== undefined) {
// send start message to background.js and receive response
chrome.runtime.sendMessage({ msg: "start", API_key: `${API_key}`, pageUrl: `${longUrl}` }, (response) => {
// store the shortened link
shortUrl = response.shortUrl;
// invalid response
if(shortUrl === 'undefined') {
document.getElementById('url__content-inner').textContent = "API Error!!";
} else {
// update the content with shortened link
document.getElementById('url__content-inner').textContent = shortUrl;
// fetch qrcode from http://goqr.me
document.getElementById('qr_code').src = `${qrcode__src}${shortUrl}`;
// show buttons
toggleDisplay('.buttons__content--holder');
}
});
}
else if (start === 'chrome') {
document.getElementById('url__content-inner').textContent = 'Not a Valid URL!!';
}
else if (API_key === '' || API_key === undefined) {
// no api key set
document.getElementById('url__content-inner').textContent = 'Set API Key in Settings!';
}
else {
document.getElementById('url__content-inner').textContent = 'Error!!!';
}
});
});
// 2. Copy Function
document.getElementById('button__copy').addEventListener("click", () => {
try {
let copyTextarea = `${shortUrl}`;
let input = document.createElement('textarea');
document.body.appendChild(input);
input.value = copyTextarea;
input.focus();
input.select();
document.execCommand('copy');
input.remove();
toggleDisplay('.copy__alert');
setTimeout(() => {
toggleDisplay('.copy__alert');
}, 1300);
}
catch (error) {
console.log('Oops, unable to copy');
}
});
// 3. Details button
document.getElementById('button__details').addEventListener('click', () => {
let win = window.open(`${shortUrl}+`, '_blank');
win.focus();
});
// 4. QR Code
document.getElementById('button__qrcode').addEventListener('click', () => {
// document.getElementById('button__qrcode').style = "display: none;";
toggleDisplay('.qrcode__content--holder');
});
// 5. elements visiblity function
function toggleDisplay(className) {
let element = document.querySelector(className);
element.classList.toggle('d-none');
}
});

View File

@@ -1,35 +0,0 @@
@import url('https://fonts.googleapis.com/css?family=Montserrat:400,600');
// Colors
$color-black: #111111;
$color-white: #ffffff;
$color-light: #fcfcfc;
$color-blue: #4d5be7;
$color-light-blue: #4d5bfa;
$font-montserrat: 'Montserrat', sans-serif;
$font-weight-regular: 400;
$font-weight-bold: 600;
*, *:before, *:after {
box-sizing: border-box;
}
.d-none {
display: none !important;
}
.v-none {
visibility: hidden !important;
}
html, body {
padding: 0;
border: 0;
margin: 0;
}
body {
font-family: $font-montserrat;
}

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