Compare commits

...

444 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
108 changed files with 12384 additions and 11527 deletions

View File

@@ -1,22 +0,0 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": "49",
"firefox": "52",
"opera": "36"
}
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"regenerator": true
}
]
]
}

View File

@@ -1,30 +0,0 @@
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"windows"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
};

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/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

@@ -1,39 +1,38 @@
## Contributing Guidelines
## Assets
- [kutt.it API](https://github.com/thedevs-network/kutt#api) is used to retreive shortened URLs.
## Development
- `npm install` to install dependencies.
- To watch file changes in developement
- Chrome
- `npm run dev-chrome`
- Firefox
- `npm run dev-firefox`
- Opera
- `npm run dev-opera`
(Reload Extension Manually in the browser)
- 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" />
- ### Chrome
- ### Firefox
- Load the Add-on via `about:debugging` as temporary Add-on.
- Choose the `manifest.json` file in the extracted directory
- 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/aAL5dQg.png" />
<img width="400" src="https://i.imgur.com/dJRL7By.png" />
- ### Opera
- Load the extension via `opera:extensions`
- Check the `Developer Mode` and load as unpacked from extensions extracted directory.
- ### Firefox
<img width="400" src="https://i.imgur.com/qUwfSNJ.png" />
- 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`.
@@ -41,28 +40,17 @@
`npm run build` builds the extension for all the browsers to `extension/(browser)` directory respectively.
## Testing
Download latest `Release`
[<img src=".github/assets/direct-download.png"
alt="Direct download"
height="50">](https://github.com/abhijithvijayan/kutt-extension/releases)
height="50">](https://github.com/thedevs-network/kutt-extension/releases)
## ToDo
<hr />
- [x] Switch to Promise return Method
- [x] Fix UI issues in Firefox
- [x] Using Node-Kutt package(feature request)
- [ ] History Feature
## Self-hosted Kutt
## Note:
Shortening might 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>.
### For Opera Users
In order to install this extension from Chrome Web Store, another opera extension called **Install Chrome Extension** should be installed first.
- [Opera addon :: Install Chrome Extension](https://addons.opera.com/en/extensions/details/install-chrome-extensions/)
- [Opera addon :: Kutt](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd)
![](https://i.imgur.com/TJTisdC.png)
- **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,76 +1,128 @@
<div align="center"><img width="150" src="src/assets/logo.png" /></div>
<div align="center"><img width="150" src="source/public/assets/logo.png" /></div>
<h1 align="center">kutt-extension</h1>
<p align="center">Browser extension to to shorten URLs for <a href="https://kutt.it">Kutt.it</a></p>
<p align="center">Browser extension for <a href="https://kutt.it">Kutt.it</a> URL shortener</p>
<div align="center">
<a href="https://travis-ci.org/abhijithvijayan/kutt-extension">
<img src="https://travis-ci.org/abhijithvijayan/kutt-extension.svg?branch=master" alt="Travis Build" />
<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/abhijithvijayan/kutt-extension/releases/latest">
<img src="https://img.shields.io/github/release/abhijithvijayan/kutt-extension.svg?colorB=blue" alt="Releases" />
<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/abhijithvijayan/kutt-extension/issues?q=is%3Aopen+is%3Aissue">
<img src="https://img.shields.io/github/issues-raw/abhijithvijayan/kutt-extension.svg?colorB=lightgrey" alt="Open Issues" />
<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/abhijithvijayan/kutt-extension/issues?q=is%3Aissue+is%3Aclosed">
<img src="https://img.shields.io/github/issues-closed-raw/abhijithvijayan/kutt-extension.svg?colorB=red" alt="Closed Issues" />
<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://david-dm.org/abhijithvijayan/kutt-extension">
<img src="https://img.shields.io/david/abhijithvijayan/kutt-extension.svg?colorB=orange" alt="DEPENDENCIES" />
</a>
<a href="https://github.com/abhijithvijayan/kutt-extension/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/abhijithvijayan/kutt-extension.svg" alt="LICENSE" />
<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
- Cross Browser Support
- Minimal UI
- Instant QR Code
- Supports Password for URLs
- Cross Browser Support
- Supports Secure Passwords for URLs
- History & Incognito Feature
- Auto Copy Feature
- Free and Open Source
- WebExtensions API
- 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
[![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)](CONTRIBUTING.md#for-opera-users) | [![Yandex](https://raw.github.com/alrra/browser-logos/master/src/yandex/yandex_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) | [![vivaldi](https://raw.github.com/alrra/browser-logos/master/src/vivaldi/vivaldi_48x48.png)](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) |
--- | --- | --- | --- | --- | --- |
49 & later ✔ | 52 & later ✔ | 36 & later ✔ | Latest ✔ | Latest ✔ | Latest ✔ |
This extension uses **Manifest V3**.
<!-- ![Edge](https://raw.github.com/alrra/browser-logos/master/src/edge/edge_48x48.png) | -->
| [![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+ |
## How to use
## Installation
- Download for browser(s)
- 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/)
- Opera [Kutt :: Opera addons](CONTRIBUTING.md#for-opera-users)
- **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)
- Generate an API Key from <a href="https://kutt.it">`https://kutt.it/`</a> after signing up. (Settings page)
## How to Use
<img width="400" src="https://i.imgur.com/qQwqeH5.png" />
1. Generate an API Key from <a href="https://kutt.it">`https://kutt.it/`</a> after signing up (Settings page)
- Paste and Save this `Key` in extension's `options page` when asked.
<img width="400" src="https://i.imgur.com/qQwqeH5.png" />
<img width="250" src="https://i.imgur.com/fJasvmv.png" alt="image2" />
2. Paste and Save this `Key` in extension's `options page` when asked
## Screenshots
<div align="center">
<img width="350" src="https://i.imgur.com/n44Eytz.gif" alt="image1" />
<img width="350" src="https://i.imgur.com/FJJ2FPU.gif" alt="image2" />
<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:
- <a href="https://kutt.it">Kutt.it</a> API permits **50** URLs shortening per day using the API Key.
- Delay in the shortening might be the issue with Kutt.it API and not with the extension's.
## Development
Ensure you have [Node.js](https://nodejs.org) 20 or later installed.
```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
View the Contributing guidelines [here](CONTRIBUTING.md).
If you like my work, you can
Original Repo: [thedevs-network/kutt](https://github.com/thedevs-network/kutt)
<a href="https://www.buymeacoffee.com/abhijithvijayan" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/purple_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>
## License
## Licence
Code released under the [MIT License](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

17254
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,64 +1,76 @@
{
"name": "kutturl-extension",
"version": "1.1.0",
"main": "background.js",
"scripts": {
"dev-chrome": "cross-env NODE_ENV=development cross-env TARGET=chrome webpack --watch --mode=development",
"dev-firefox": "cross-env NODE_ENV=development cross-env TARGET=firefox webpack --watch --mode=development",
"dev-opera": "cross-env NODE_ENV=development cross-env TARGET=opera webpack --watch --mode=development",
"build-chrome": "cross-env NODE_ENV=production cross-env TARGET=chrome webpack --mode=production",
"build-firefox": "cross-env NODE_ENV=production cross-env TARGET=firefox webpack --mode=production",
"build-opera": "cross-env NODE_ENV=production cross-env TARGET=opera webpack --mode=production",
"build": "npm run build-chrome && npm run build-firefox && npm run build-opera"
},
"author": "abhijithvijayan",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/plugin-transform-runtime": "^7.2.0",
"@babel/preset-env": "^7.3.1",
"autoprefixer": "^9.4.7",
"babel-loader": "^8.0.5",
"clean-webpack-plugin": "^1.0.1",
"copy-webpack-plugin": "^4.6.0",
"cross-env": "^5.2.0",
"css-loader": "^2.1.0",
"eslint": "^5.13.0",
"extract-loader": "^3.1.0",
"file-loader": "^3.0.1",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^4.0.0-beta.5",
"node-sass": "^4.11.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"postcss-loader": "^3.0.0",
"precss": "^4.0.0",
"resolve-url-loader": "^3.0.0",
"sass-loader": "^7.1.0",
"terser-webpack-plugin": "^1.2.2",
"url-loader": "^1.1.2",
"webpack": "^4.29.0",
"webpack-cli": "^3.2.1",
"webpack-dev-server": "^3.1.14",
"zip-webpack-plugin": "^3.0.0"
},
"dependencies": {
"@babel/runtime": "^7.3.1",
"axios": "^0.18.0",
"kutt": "^1.0.3",
"qrcode": "^1.3.3",
"webextension-polyfill": "^0.3.1"
},
"name": "kutt-extension",
"version": "4.4.2",
"description": "Kutt.it extension for browsers.",
"repository": {
"type": "git",
"url": "git+https://github.com/abhijithvijayan/kutt-extension.git"
"license": "MIT",
"repository": "https://github.com/thedevs-network/kutt-extension.git",
"author": {
"name": "abhijithvijayan",
"email": "email@abhijithvijayan.in",
"url": "https://abhijithvijayan.in"
},
"engines": {
"node": ">=20"
},
"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/kutt-extension/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/kutt-extension#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="chart-line" class="svg-inline--fa fa-chart-line fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#888" d="M496 384H64V80c0-8.84-7.16-16-16-16H16C7.16 64 0 71.16 0 80v336c0 17.67 14.33 32 32 32h464c8.84 0 16-7.16 16-16v-32c0-8.84-7.16-16-16-16zM464 96H345.94c-21.38 0-32.09 25.85-16.97 40.97l32.4 32.4L288 242.75l-73.37-73.37c-12.5-12.5-32.76-12.5-45.25 0l-68.69 68.69c-6.25 6.25-6.25 16.38 0 22.63l22.62 22.62c6.25 6.25 16.38 6.25 22.63 0L192 237.25l73.37 73.37c12.5 12.5 32.76 12.5 45.25 0l96-96 32.4 32.4c15.12 15.12 40.97 4.41 40.97-16.97V112c.01-8.84-7.15-16-15.99-16z"></path></svg>

Before

Width:  |  Height:  |  Size: 680 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="#888" 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: 710 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 fill="#444" 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="#888" 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: 425 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="#444" 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,46 +0,0 @@
{
"manifest_version": 2,
"name": "Kutt",
"version": "0.8.0",
"description": "URL Shortener",
"icons": {
"16": "assets/favicon-16.png",
"32": "assets/favicon-32.png",
"48": "assets/favicon-48.png",
"128": "assets/favicon-128.png"
},
"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"
},
"default_title": "Kutt",
"chrome_style": false
},
"author": "abhijithvijayan",
"background": {
"persistent": false,
"scripts": [
"js/background.js"
]
},
"homepage_url": "https://github.com/abhijithvijayan/kutt-extension",
"minimum_chrome_version": "49",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"options_page": "options.html",
"options_ui": {
"page": "options.html",
"chrome_style": false,
"open_in_tab": true
},
"permissions": [
"tabs",
"storage",
"clipboardRead",
"https://kutt.it/*/*"
],
"short_name": "Kutt"
}

View File

@@ -1,48 +0,0 @@
{
"manifest_version": 2,
"name": "Kutt",
"version": "0.8.0",
"browser_specific_settings": {
"gecko": {
"id": "support@kutt.it"
}
},
"description": "URL Shortener",
"icons": {
"16": "assets/favicon-16.png",
"32": "assets/favicon-32.png",
"48": "assets/favicon-48.png",
"128": "assets/favicon-128.png"
},
"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"
},
"default_title": "Kutt",
"browser_style": false
},
"author": "abhijithvijayan",
"background": {
"scripts": [
"js/background.js"
]
},
"homepage_url": "https://github.com/abhijithvijayan/kutt-extension",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"permissions": [
"tabs",
"storage",
"clipboardWrite",
"clipboardRead",
"https://kutt.it/*/*"
],
"short_name": "Kutt"
}

View File

@@ -1,47 +0,0 @@
{
"manifest_version": 2,
"name": "Kutt",
"version": "0.8.0",
"description": "URL Shortener",
"developer": {
"name": "abhijithvijayan"
},
"icons": {
"16": "assets/favicon-16.png",
"32": "assets/favicon-32.png",
"48": "assets/favicon-48.png",
"128": "assets/favicon-128.png"
},
"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"
},
"default_title": "Kutt",
"chrome_style": false
},
"background": {
"persistent": false,
"scripts": [
"js/background.js"
]
},
"homepage_url": "https://github.com/abhijithvijayan/kutt-extension",
"minimum_opera_version": "36",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"options_page": "options.html",
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"permissions": [
"tabs",
"storage",
"clipboardRead",
"https://kutt.it/*/*"
],
"short_name": "Kutt"
}

View File

@@ -1,57 +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: Kutt</title>
<link rel="stylesheet" href="css/options.css" />
</head>
<body>
<section id="options">
<div class="container options__content--holder">
<div class="head__content--holder text-center">
<img class="head__content--logo" src="assets/logo.png" />
<a class="head__content--title" href="https://kutt.it" target="_blank" rel="noopener">Kutt.it</a>
</div>
<div class="form__content--holder">
<form class="form__content">
<label class="api__key--label">API Key:
<a class="api__label--Qmark" target="_blank" rel="noopener" href="https://kutt.it/login">?
<span class="api__label--tooltiptext text-center">
Generate key from Kutt.it&nbsp;Website<br>(Settings Page)
</span>
</a>
</label>
<input class="api__key--holder" id="api__key--value" type="password" spellcheck="false" />
<br />
<label class="password--label">Set Password
<span class="password__label--optional">(Optional):
<span class="password__label--tooltiptext text-center">
Set Password for the Shortened URLs.<br /> (20 Char. Max)
</span>
</span>
</label>
<input class="password--holder" id="password--value" type="password" maxlength="20" />
<div class="password__check--holder">
<input type="checkbox" id="password__view--checkbox">
<span class="password__view--title">Show Password</span>
</div>
<br />
<button class="button__submit" id="button__submit" type="button">Save</button>
<label class="saved__alert v-none">Saved!!</label>
</form>
</div>
<div class="footer__text--holder text-center">
Made with ❤️ on <a class="github__repo--link" href="https://github.com/abhijithvijayan/kutt-extension" target="_blank"
rel="noopener">GitHub</a>
</div>
</div>
</section>
<script src="js/options.js"></script>
</body>
</html>

View File

@@ -1,49 +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>Kutt</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 selectDisable" src="assets/logo.png">
</li>
<li class="options__content-holder" id="options__content-holder" title="Options">
<a href="options.html" target="_blank" rel="noopener">
<img class="settings__logo selectDisable" src="assets/settings.svg"></a>
</li>
</ul>
</nav>
<div class="content__holder">
<div class="url__content--holder text-center">
<h4 id="url__content-inner">Shortening...</h4>
<ul class="buttons__content--holder d-none">
<li class="copy__content--holder" id="button__copy--holder" title="Copy">
<img id="button__copy" class="selectDisable" src="assets/copy.svg" alt="copy" />
</li>
<li class="qrbtn__content--holder" id="button__qrcode--holder" title="QR code">
<img id="button__qrcode" class="selectDisable" src="assets/qrcode.svg" alt="QR Code" />
</li>
</ul>
</div>
<p class="copy__alert text-center d-none">Copied!!!</p>
<div class="qrcode__content--holder text-center d-none">
<img id="qr_code" src="#" alt="QRCode" />
</div>
</div>
</div>
</section>
<script src="js/popup.js"></script>
</body>
</html>

View File

@@ -1,40 +0,0 @@
import Kutt from 'kutt';
import browser from 'webextension-polyfill';
// Shorten url
async function getShortURL(API_key, URLtoShorten, password) {
const kutt = new Kutt();
kutt.setKey(API_key);
const data = {
target: URLtoShorten,
password: password
};
try {
const response = await kutt.submit(data);
// Returning shortlink
return response.shortUrl;
} catch (e) {
// time out
if (e.code === 'ECONNABORTED') {
return 504;
}
else if (e.response) {
// return error code
return e.response.status;
}
}
}
// Calling function
browser.runtime.onMessage.addListener(async (request, sender, response) => {
if (request.msg == 'start') {
// consume the promise
return getShortURL(request.API_key, request.pageUrl, request.password)
.then(shortLink => {
return shortLink;
});
}
});

View File

@@ -1,62 +0,0 @@
import browser from 'webextension-polyfill';
// update UI - API Key on options page load
document.addEventListener('DOMContentLoaded', () => {
// replace the input value with current value on load
browser.storage.local.get(['key', 'pwd']).then(result => {
// to strings
let API_KEY = `${result.key}`, pwd = `${result.pwd}`;
if (API_KEY === 'undefined') {
document.getElementById('api__key--value').value = '';
} else {
document.getElementById('api__key--value').value = API_KEY;
document.getElementById('password--value').value = pwd;
}
});
});
// Store Data and alert message
let saveData = () => {
let API_KEY = document.getElementById('api__key--value').value;
let password = document.getElementById('password--value').value;
// store value locally
browser.storage.local.set({ key: API_KEY, pwd: password }).then(() => { });
// Saved Alert
let element = document.querySelector('.saved__alert');
element.classList.toggle('v-none');
setTimeout(() => {
element.classList.toggle('v-none');
// close current tab
browser.tabs.getCurrent().then((tabInfo) => {
browser.tabs.remove(tabInfo.id);
});
}, 1250);
};
// on save button click
document.getElementById('button__submit').addEventListener('click', () => {
saveData();
});
// on enter key press
document.addEventListener('keypress', (e) => {
if (e.keyCode === 13) {
saveData();
}
});
// Show Password
document.getElementById('password__view--checkbox').addEventListener('click', () => {
let element = document.getElementById('password--value');
if (element.type === 'password') {
element.type = 'text';
} else {
element.type = 'password';
}
});

View File

@@ -1,122 +0,0 @@
import browser from 'webextension-polyfill';
import QRCode from 'qrcode';
let shortUrl;
document.addEventListener('DOMContentLoaded', () => {
// 1. Initialize
browser.tabs.query({ 'active': true, 'lastFocusedWindow': true }).then(tabs => {
let longUrl, start, qrcode__src = 'https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=';
let API_key, password;
longUrl = tabs[0].url;
start = longUrl.substr(0, 4);
// i) Get api key from options page
browser.storage.local.get(['key', 'pwd']).then(result => {
API_key = result.key;
password = result.pwd;
// update DOM
let updateContent = (value) => {
document.getElementById('url__content-inner').textContent = value;
};
if (start === 'http' && API_key !== '' && API_key !== undefined) {
// send start message to background.js and receive response
browser.runtime.sendMessage({ msg: 'start', API_key: `${API_key}`, pageUrl: `${longUrl}`, password: `${password}` }).then(response => {
// store the shortened link
shortUrl = response;
// status codes
if (!isNaN(shortUrl)) {
if (shortUrl === 429) {
updateContent('API Limit Exceeded!');
} else if (shortUrl === 401) {
updateContent('Invalid API Key');
// } else if (shortUrl === 400) {
// updateContent('Unknown Error!!!');
} else if (shortUrl === 504) {
updateContent('Time-out!');
} else {
updateContent('Unknown Error!!!');
}
}
// valid response
else if (shortUrl !== null) {
// 1. update the content with shortened link
updateContent(shortUrl);
// 2. show buttons
toggleDisplay('.buttons__content--holder');
// 3. QR Code Generation
QRCode.toDataURL(shortUrl)
.then(url => {
document.getElementById('qr_code').src = url;
})
.catch(err => {
// fetch qrcode from http://goqr.me
document.getElementById('qr_code').src = `${qrcode__src}${shortUrl}`;
});
}
else {
updateContent('Invalid Response!');
}
});
}
else if (API_key === '' || API_key === undefined) {
// no api key set
updateContent('Set API Key in Options!');
// open options page
setTimeout(() => {
browser.runtime.openOptionsPage();
}, 900);
}
else if (start !== 'http') {
updateContent('Not a Valid URL!!');
}
});
});
// 2. Copy Function
document.getElementById('button__copy--holder').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. QR Code
document.getElementById('button__qrcode--holder').addEventListener('click', () => {
toggleDisplay('.qrcode__content--holder');
});
// 4. elements visiblity function
function toggleDisplay(className) {
let element = document.querySelector(className);
element.classList.toggle('d-none');
}
});

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