Merge pull request #80 from abhijithvijayan/refactor/react-typescript

TypeScript + React Complete Rewrite
This commit is contained in:
Abhijith Vijayan
2020-02-09 21:58:15 +05:30
committed by GitHub
65 changed files with 150589 additions and 5214 deletions

View File

@@ -1,22 +1,30 @@
{
"presets": [
[
// Latest stable ECMAScript features
"@babel/preset-env",
{
"targets": {
"chrome": "49",
"firefox": "52",
"opera": "36"
"opera": "36",
"edge": "79"
}
}
]
],
"plugins": [
// Some transforms (such as object-rest-spread)
// don't work without it: https://github.com/babel/babel/issues/7215
["@babel/plugin-transform-destructuring", { "useBuiltIns": true }],
["@babel/plugin-proposal-object-rest-spread", { "useBuiltIns": true }],
[
// Polyfills the runtime needed for async/await and generators
"@babel/plugin-transform-runtime",
{
"helpers": false,
"regenerator": true
}
]
]
}
}

View File

@@ -1,2 +1,5 @@
node_modules/
extension/
dist/
extension/
.yarn/
.pnp.js

View File

@@ -1,5 +1,27 @@
{
"extends": [
"onepass"
]
}
"extends":["onepass"],
"rules": {
"no-console": 0,
"no-extend-native": 0,
"react/jsx-filename-extension": [1, { "extensions": [".jsx", "tsx"] }],
"react/jsx-props-no-spreading": 0,
"jsx-a11y/label-has-associated-control": 0,
"prettier/prettier": [
"error",
{
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 4
}
]
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"],
"moduleDirectory": ["node_modules", "src/"]
}
}
}
}

11
.gitignore vendored
View File

@@ -66,3 +66,14 @@ typings/
## scripts build
extension/
dist/
# awesome-ts-loader cache
.awcache
# yarn 2
# https://github.com/yarnpkg/berry/issues/454#issuecomment-530312089
.yarn/*
!.yarn/releases
!.yarn/plugins
.pnp.*

2
.nvmrc
View File

@@ -1 +1 @@
12.13.1
12

147315
.yarn/releases/yarn-1.21.1.js vendored Executable file

File diff suppressed because one or more lines are too long

1
.yarnrc.yml Normal file
View File

@@ -0,0 +1 @@
yarnPath: .yarn/releases/yarn-1.21.1.js

View File

@@ -1,8 +1,6 @@
<div align="center"><img width="150" src="src/assets/logo.png" /></div>
<h1 align="center">kutt-extension</h1>
<p align="center">Browser extension to shorten long URLs based on <a href="https://kutt.it">Kutt.it</a></p>
<h3 align="center">🙋‍♂️ Made by <a href="https://twitter.com/_abhijithv">@abhijithvijayan</a></h3>
<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" />
@@ -23,8 +21,20 @@
<img src="https://img.shields.io/github/license/abhijithvijayan/kutt-extension.svg" alt="LICENSE" />
</a>
</div>
<h3 align="center">🙋‍♂️ Made by <a href="https://twitter.com/_abhijithv">@abhijithvijayan</a></h3>
<p align="center">
Donate:
<a href="https://www.paypal.me/iamabhijithvijayan" target='_blank'><i><b>PayPal</b></i></a>,
<a href="https://www.patreon.com/abhijithvijayan" target='_blank'><i><b>Patreon</b></i></a>
</p>
<p align="center">
<a href='https://www.buymeacoffee.com/abhijithvijayan' target='_blank'>
<img height='36' style='border:0px;height:36px;' src='https://bmc-cdn.nyc3.digitaloceanspaces.com/BMC-button-images/custom_images/orange_img.png' border='0' alt='Buy Me a Coffee' />
</a>
</p>
<hr />
## v2 (🚧 [WIP](https://github.com/abhijithvijayan/kutt-extension/tree/refactor/react-typescript))
### v2 React + TypeScript (🚧 [WIP](https://github.com/abhijithvijayan/kutt-extension/tree/refactor/react-typescript))
## Features
@@ -39,10 +49,9 @@
## 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) |
![Edge](https://raw.github.com/alrra/browser-logos/master/src/edge/edge_48x48.png) |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 49 & later ✔ | 52 & later ✔ | 36 & later ✔ | Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔
| [![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) | [![Edge](https://raw.github.com/alrra/browser-logos/master/src/edge/edge_48x48.png)](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) | [![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 ✔ | 79 & later ✔ | Latest ✔ | Latest ✔ | Latest ✔
## How to use

View File

@@ -1,15 +1,34 @@
{
"name": "kutt-extension",
"version": "3.2.1",
"main": "src/scripts/background.js",
"private": true,
"license": "MIT",
"engines": {
"node": ">= 8 <=12"
},
"author": "abhijithvijayan",
"description": "Kutt.it extension for browsers.",
"repository": "git+https://github.com/abhijithvijayan/kutt-extension.git",
"engines": {
"node": ">=8 <=12",
"yarn": ">= 1.0.0"
},
"repository": "https://github.com/abhijithvijayan/kutt-extension.git",
"author": "abhijithvijayan <34790378+abhijithvijayan@users.noreply.github.com>",
"scripts": {
"dev:chrome": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome webpack --watch",
"dev:firefox": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=firefox webpack --watch",
"dev:opera": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=opera webpack --watch",
"build:chrome": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome webpack",
"build:firefox": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=firefox webpack",
"build:opera": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=opera webpack",
"build": "yarn run build:chrome && yarn run build:firefox && yarn run build:opera",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"npm run lint:fix"
]
},
"keywords": [
"url",
"shortener",
@@ -18,67 +37,60 @@
"addon",
"kutt"
],
"bugs": {
"url": "https://github.com/abhijithvijayan/kutt-extension/issues"
},
"homepage": "https://github.com/abhijithvijayan/kutt-extension#readme",
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"dev:chrome": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome webpack --watch --mode=development",
"dev:firefox": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=firefox webpack --watch --mode=development",
"dev:opera": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=opera webpack --watch --mode=development",
"build:chrome": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome webpack --mode=production",
"build:firefox": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=firefox webpack --mode=production",
"build:opera": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=opera webpack --mode=production",
"build": "yarn run build:chrome && yarn run build:firefox && yarn run build:opera"
},
"license": "MIT",
"private": true,
"dependencies": {
"@babel/runtime": "^7.7.6",
"axios": "^0.19.0",
"qrcode": "^1.4.4",
"webextension-polyfill": "^0.5.0",
"wext-manifest": "^2.0.1"
"@babel/runtime": "^7.8.3",
"axios": "^0.19.2",
"formik": "^2.1.3",
"lodash.isequal": "^4.5.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"webextension-polyfill-ts": "^0.12.0"
},
"devDependencies": {
"@babel/core": "^7.7.5",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.6",
"autoprefixer": "^9.7.3",
"@babel/core": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/plugin-transform-destructuring": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.8.3",
"@types/lodash.isequal": "^4.5.5",
"@types/react": "^16.9.19",
"@types/react-dom": "^16.9.5",
"@types/webpack": "^4.41.3",
"@typescript-eslint/eslint-plugin": "^2.17.0",
"@typescript-eslint/parser": "^2.17.0",
"autoprefixer": "^9.7.4",
"awesome-typescript-loader": "^5.2.1",
"babel-eslint": "10.0.3",
"babel-loader": "^8.0.6",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.1.0",
"copy-webpack-plugin": "^5.1.1",
"cross-env": "^6.0.3",
"css-loader": "^3.3.0",
"eslint": "^6.7.2",
"css-loader": "^3.4.2",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-config-onepass": "1.5.0",
"eslint-config-prettier": "^6.7.0",
"eslint-config-onepass": "2.1.0",
"eslint-config-prettier": "^6.9.0",
"eslint-plugin-html": "^6.0.0",
"eslint-plugin-import": "^2.19.1",
"eslint-plugin-import": "^2.20.0",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-react": "^7.17.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.18.0",
"eslint-plugin-react-hooks": "^2.3.0",
"extract-loader": "^3.1.0",
"file-loader": "^5.0.2",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^4.0.0-beta.5",
"node-sass": "^4.13.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"html-webpack-plugin": "^3.2.0",
"husky": "^4.2.1",
"lint-staged": "^10.0.3",
"mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.13.1",
"postcss-loader": "^3.0.0",
"precss": "^4.0.0",
"prettier": "^1.19.1",
"resolve-url-loader": "^3.1.1",
"sass-loader": "^7.3.1",
"terser-webpack-plugin": "^1.4.3",
"url-loader": "^2.3.0",
"webpack": "^4.41.2",
"sass-loader": "^8.0.2",
"typescript": "^3.7.5",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
"webpack-fix-style-only-entries": "^0.3.1",
"write-webpack-plugin": "^1.1.0",
"zip-webpack-plugin": "^3.0.0"
"webpack-extension-reloader": "^1.1.4",
"wext-manifest": "^2.1.0",
"write-webpack-plugin": "^1.1.0"
}
}

View File

@@ -0,0 +1,5 @@
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

177
src/Background/index.ts Normal file
View File

@@ -0,0 +1,177 @@
/**
* @author abhijithvijayan <abhijithvijayan.in>
*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { browser } from 'webextension-polyfill-ts';
import { AxiosPromise } from 'axios';
import * as constants from './constants';
import api from '../api';
type ShortenUrlBodyProperties = {
target: string;
password?: string;
customurl?: string;
reuse: boolean;
domain?: string;
};
export interface ApiBodyProperties extends ShortenUrlBodyProperties {
apikey: 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 type ApiErroredProperties = {
error: true;
message: string;
};
export type SuccessfulShortenStatusProperties = {
error: false;
data: ShortenLinkResponseProperties;
};
async function shortenUrl(
params: ApiBodyProperties
): Promise<SuccessfulShortenStatusProperties | ApiErroredProperties> {
try {
// extract `apikey` from body
const { apikey, ...otherParams } = params;
const { data }: { data: ShortenLinkResponseProperties } = await api({
method: 'POST',
timeout: constants.SHORTEN_URL_TIMEOUT,
url: `/api/v2/links`,
headers: {
'X-API-Key': apikey,
},
data: {
...otherParams,
},
});
return {
error: false,
data,
};
} catch (err) {
if (err.response) {
if (err.response.status === 401) {
return {
error: true,
message: 'Error: Invalid API Key',
};
}
}
if (err.code === 'ECONNABORTED') {
return {
error: true,
message: 'Error: Timed out',
};
}
return {
error: true,
message: 'Error: Something went wrong',
};
}
}
function getUserSettings(apikey: string): AxiosPromise<any> {
return api({
method: 'GET',
url: '/api/v2/users',
timeout: constants.CHECK_API_KEY_TIMEOUT,
headers: {
'X-API-Key': apikey,
},
});
}
export type DomainEntryProperties = {
address: string;
banned: boolean;
created_at: string;
id: string;
homepage: string;
updated_at: string;
};
export type UserSettingsResponseProperties = {
apikey: string;
email: string;
domains: DomainEntryProperties[];
};
export type SuccessfulApiKeyCheckProperties = {
error: false;
data: UserSettingsResponseProperties;
};
async function checkApiKey(apikey: string): Promise<SuccessfulApiKeyCheckProperties | ApiErroredProperties> {
try {
const { data }: { data: UserSettingsResponseProperties } = await getUserSettings(apikey);
return {
error: false,
data,
};
} catch (err) {
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: Please check your internet connection',
};
}
}
/**
* Listen for messages from UI
*/
browser.runtime.onMessage.addListener((request, sender): void | Promise<any> => {
console.log('message received', request);
// eslint-disable-next-line default-case
switch (request.action) {
case constants.CHECK_API_KEY: {
return checkApiKey(request.params.apikey);
}
case constants.SHORTEN_URL: {
return shortenUrl(request.params);
}
}
});

45
src/Options/Options.tsx Normal file
View File

@@ -0,0 +1,45 @@
import React, { useEffect, useState } from 'react';
import { getExtensionSettings } from '../util/settings';
import BodyWrapper from '../components/BodyWrapper';
import Loader from '../components/Loader';
import OptionsForm, { OptionsFormValuesProperties } from './OptionsForm';
const Options: React.FC = () => {
const [loading, setLoading] = useState(true);
const [defaultValues, setDefaultValues] = useState<OptionsFormValuesProperties>({
apikey: '',
autocopy: true,
history: false,
});
useEffect(() => {
async function getSavedSettings(): Promise<void> {
const { settings = {} } = await getExtensionSettings();
// inject existing keys (if field doesn't exist, use default)
const defaultFormValues: OptionsFormValuesProperties = {
apikey: settings.apikey || defaultValues.apikey,
autocopy: Object.prototype.hasOwnProperty.call(settings, 'autocopy')
? settings.autocopy
: defaultValues.autocopy,
history: Object.prototype.hasOwnProperty.call(settings, 'history')
? settings.history
: defaultValues.history,
};
setDefaultValues(defaultFormValues);
setLoading(false);
}
getSavedSettings();
}, [defaultValues.apikey, defaultValues.autocopy, defaultValues.history]);
return (
<BodyWrapper>
<div id="options">{!loading ? <OptionsForm defaultValues={defaultValues} /> : <Loader />}</div>
</BodyWrapper>
);
};
export default Options;

115
src/Options/OptionsForm.tsx Normal file
View File

@@ -0,0 +1,115 @@
import React from 'react';
import { withFormik, Field, Form, FormikHelpers, FormikProps, FormikErrors } from 'formik';
import AutoSave from '../util/autoSave';
import messageUtil from '../util/mesageUtil';
import { CHECK_API_KEY } from '../Background/constants';
import { TextField, CheckBox } from '../components/Input';
import { updateExtensionSettings } from '../util/settings';
import { SuccessfulApiKeyCheckProperties, ApiErroredProperties } from '../Background';
export type OptionsFormValuesProperties = {
apikey: string;
autocopy: boolean;
history: boolean;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const onSave = (values: OptionsFormValuesProperties): Promise<any> => {
// should always return a Promise
return updateExtensionSettings(values); // update local settings
};
// Note: The default key-value pairs are not saved to storage without any first interaction
const InnerForm: React.FC<FormikProps<OptionsFormValuesProperties>> = props => {
// ToDo: Replace `Saving` text with Spinning Loader
const { isSubmitting, handleSubmit } = props;
return (
<Form onSubmit={handleSubmit} autoComplete="off" id="options__form">
<div>
<Field name="apikey" type="password" component={TextField} label="API Key" />
<button type="submit" disabled={isSubmitting}>
Validate
</button>
</div>
<div>
<Field name="autocopy" component={CheckBox} label="Auto Copy URL to Clipboard" />
</div>
<div>
<Field name="history" component={CheckBox} label="Keep URLs History" />
</div>
<AutoSave
onSave={onSave}
render={({ isSaving }: { isSaving: boolean }): string | null => {
return isSaving ? 'Saving' : null;
}}
/>
</Form>
);
};
// The type of props `OptionsForm` receives
type OptionsFormProperties = {
defaultValues: OptionsFormValuesProperties;
};
// Wrap our form with the withFormik HoC
const OptionsForm = withFormik<OptionsFormProperties, OptionsFormValuesProperties>({
// Transform outer props into form values
mapPropsToValues: ({ defaultValues: { apikey, autocopy, history } }) => {
return {
apikey,
autocopy,
history,
};
},
validate: (values: OptionsFormValuesProperties) => {
const errors: FormikErrors<OptionsFormValuesProperties> = {};
if (!values.apikey) {
errors.apikey = 'API key missing';
}
// ToDo: restore before on production
// else if (values.apikey && values.apikey.trim().length < 40) {
// errors.apikey = 'API key must be 40 characters';
// } else if (values.apikey && values.apikey.trim().length > 40) {
// errors.apikey = 'API key cannot exceed 40 characters';
// }
return errors;
},
// for API Key validation only
handleSubmit: async (
values: OptionsFormValuesProperties,
{ setSubmitting }: FormikHelpers<OptionsFormValuesProperties>
) => {
const response: SuccessfulApiKeyCheckProperties | ApiErroredProperties = await messageUtil.send(CHECK_API_KEY, {
apikey: values.apikey.trim(),
});
if (!response.error) {
// ToDo: show valid api key status
console.log('Valid API Key');
const { domains, email } = response.data;
// Store user account information
await updateExtensionSettings({ user: { domains, email } });
} else {
// ---- errored ---- //
// Delete `user` field from settings
await updateExtensionSettings({ user: null });
console.log(response.message);
}
// enable validate button
setSubmitting(false);
},
displayName: 'OptionsForm',
})(InnerForm);
export default OptionsForm;

8
src/Options/index.tsx Normal file
View File

@@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './styles.scss';
import Options from './Options';
ReactDOM.render(<Options />, document.getElementById('options-root'));

38
src/Options/styles.scss Normal file
View File

@@ -0,0 +1,38 @@
@import "../styles/fonts";
@import "../styles/reset";
@import "../styles/variables";
body {
color: $black;
background-color: $grey-white;
}
#options {
display: flex;
justify-content: center;
padding: 20% 10%;
font-size: 1.125em;
#options__form {
button {
padding: 7px 22px;
margin: 15px auto;
cursor: pointer;
background-color: lightblue;
}
div {
padding: 5px 10px;
width: 100%;
.error {
font-size: 11px;
padding: 2px 10px;
}
input[type="checkbox"] {
width: auto;
}
}
}
}

23
src/Popup/Header.tsx Normal file
View File

@@ -0,0 +1,23 @@
import React from 'react';
import Icon from '../components/Icon';
import { openExtOptionsPage } from '../util/tabs';
const Header: React.FC = () => {
return (
<>
<header id="header">
<div className="logo__holder">
<img src="assets/logo.png" alt="logo" style={{ width: '22px', height: '22px' }} />
</div>
<div className="action__buttons--holder">
<button type="button" className="icon" onClick={openExtOptionsPage}>
<Icon name="settings" />
</button>
</div>
</header>
</>
);
};
export default Header;

123
src/Popup/Popup.tsx Normal file
View File

@@ -0,0 +1,123 @@
import React, { useEffect, useState } from 'react';
import { UserSettingsResponseProperties } from '../Background';
import { getExtensionSettings } from '../util/settings';
import BodyWrapper from '../components/BodyWrapper';
import Loader from '../components/Loader';
import PopupForm from './PopupForm';
import PopupHeader from './Header';
import PopupBody, { ProcessedRequestProperties } from './PopupBody';
import './styles.scss';
type DomainOptionsProperties = {
option: string;
value: string;
id: string;
disabled: boolean;
};
export type ProcessRequestProperties = React.Dispatch<
React.SetStateAction<{
error: boolean | null;
message: string;
}>
>;
export type UserConfigProperties = {
apikey: string;
domainOptions: DomainOptionsProperties[];
};
const Popup: React.FC = () => {
const [loading, setLoading] = useState<boolean>(true);
const [userConfig, setUserConfig] = useState<UserConfigProperties>({
apikey: '',
domainOptions: [],
});
const [requestProcessed, setRequestProcessed] = useState<ProcessedRequestProperties>({ error: null, message: '' });
useEffect((): void => {
async function getUserSettings(): Promise<void> {
// ToDo: type
const { settings = {} } = await getExtensionSettings();
// ToDo: change kutt.it entry to custom host(if exist)
const defaultOptions: DomainOptionsProperties[] = [
{
id: '',
option: '-- Choose Domain --',
value: '',
disabled: true,
},
{
id: 'default',
option: 'kutt.it',
value: 'https://kutt.it',
disabled: false,
},
];
// No API Key set
if (!Object.prototype.hasOwnProperty.call(settings, 'apikey') || settings.apikey === '') {
setRequestProcessed({ error: true, message: 'Extension requires an API Key to work' });
setLoading(false);
// ToDo: Open options page after slight delay
return;
}
// `user` & `apikey` fields exist on storage
if (Object.prototype.hasOwnProperty.call(settings, 'user') && settings.user) {
const { user }: { user: UserSettingsResponseProperties } = settings;
let optionsArray: DomainOptionsProperties[] = user.domains.map(({ id, address, homepage, banned }) => {
return {
id,
option: homepage,
value: address,
disabled: banned,
};
});
// merge to beginning of array
optionsArray = defaultOptions.concat(optionsArray);
// update domain list
setUserConfig({ apikey: settings.apikey, domainOptions: optionsArray });
} else {
// no `user` but `apikey` exist on storage
setUserConfig({ apikey: settings.apikey, domainOptions: defaultOptions });
}
// ToDo: handle init operations(if any)
setLoading(false);
}
getUserSettings();
}, []);
return (
<BodyWrapper>
<div id="popup">
{!loading ? (
<>
<PopupHeader />
{(requestProcessed.error !== null && (
<PopupBody requestProcessed={requestProcessed} setRequestProcessed={setRequestProcessed} />
)) || (
<PopupForm
defaultDomainId="default"
userConfig={userConfig}
setRequestProcessed={setRequestProcessed}
/>
)}
</>
) : (
<Loader />
)}
</div>
</BodyWrapper>
);
};
export default Popup;

35
src/Popup/PopupBody.tsx Normal file
View File

@@ -0,0 +1,35 @@
import React from 'react';
import { ProcessRequestProperties } from './Popup';
export type ProcessedRequestProperties = {
error: boolean | null;
message: string;
};
type PopupBodyProperties = {
requestProcessed: ProcessedRequestProperties;
setRequestProcessed: ProcessRequestProperties;
};
const PopupBody: React.FC<PopupBodyProperties> = ({ requestProcessed: { message, error }, setRequestProcessed }) => {
return (
<>
<div>
{!error ? (
<button
type="button"
onClick={(): void => {
return setRequestProcessed({ error: null, message: '' });
}}
>
Go Back
</button>
) : null}
<p>{message}</p>
</div>
</>
);
};
export default PopupBody;

154
src/Popup/PopupForm.tsx Normal file
View File

@@ -0,0 +1,154 @@
import React from 'react';
import { withFormik, Field, Form, FormikBag, FormikProps, FormikErrors } from 'formik';
import Loader from '../components/Loader';
import messageUtil from '../util/mesageUtil';
import { UserConfigProperties, ProcessRequestProperties } from './Popup';
import { getCurrentTab } from '../util/tabs';
import { SHORTEN_URL } from '../Background/constants';
import { SelectField, TextField } from '../components/Input';
import { ApiBodyProperties, SuccessfulShortenStatusProperties, ApiErroredProperties } from '../Background';
type PopupFormValuesProperties = {
password: string;
customurl: string;
domain: string;
};
const InnerForm: React.FC<PopupFormProperties & FormikProps<PopupFormValuesProperties>> = props => {
const {
isSubmitting,
handleSubmit,
userConfig: { domainOptions },
} = props;
return (
<>
{isSubmitting ? (
<Loader />
) : (
<Form onSubmit={handleSubmit} autoComplete="off" id="popup__form">
<div>
<Field
name="domain"
type="text"
component={SelectField}
label="Domain"
options={domainOptions}
/>
</div>
<div>
<Field name="customurl" type="text" component={TextField} label="Custom URL" />
</div>
<div>
<Field name="password" type="password" component={TextField} label="Password" />
</div>
<button type="submit" disabled={isSubmitting}>
Create
</button>
</Form>
)}
</>
);
};
// The type of props `PopupForm` receives
type PopupFormProperties = {
defaultDomainId: string;
userConfig: UserConfigProperties;
setRequestProcessed: ProcessRequestProperties;
};
// Wrap our form with the withFormik HoC
const PopupForm = withFormik<PopupFormProperties, PopupFormValuesProperties>({
// Transform outer props into default form values
mapPropsToValues: ({
defaultDomainId,
userConfig: { domainOptions },
}: PopupFormProperties): PopupFormValuesProperties => {
// find default item to select in options menu
const defaultItem = domainOptions.find(({ id }) => {
return id === defaultDomainId;
});
return {
password: '',
customurl: '',
domain: defaultItem && defaultItem.value ? defaultItem.value.trim() : '', // empty string will map to disabled entry
};
},
// Custom sync validation
validate: (values: PopupFormValuesProperties): FormikErrors<PopupFormValuesProperties> => {
const errors: FormikErrors<PopupFormValuesProperties> = {};
// ToDo: Remove special symbols from password & customurl fields
// password validation
if (values.password && values.password.trim().length < 3) {
errors.password = 'Password must be atleast 3 characters';
}
// custom url validation
if (values.customurl && values.customurl.trim().length < 3) {
errors.customurl = 'Custom URL must be atleast 3 characters';
}
return errors;
},
handleSubmit: async (
values: PopupFormValuesProperties,
{
setSubmitting,
props: {
setRequestProcessed,
userConfig: { apikey },
},
}: FormikBag<PopupFormProperties, PopupFormValuesProperties>
) => {
// Get target link to shorten
const tabs = await getCurrentTab();
const target: string | null = (tabs.length > 0 && tabs[0].url) || null;
if (!target || !target.startsWith('http')) {
// No valid target
return setRequestProcessed({ error: true, message: 'Not a valid URL' });
}
const { customurl, password, domain } = values;
const apiBody: ApiBodyProperties = {
apikey,
target,
...(customurl.trim() !== '' && { customurl: customurl.trim() }), // add this key only if field is not empty
...(password.trim() !== '' && { password: password.trim() }),
reuse: false,
...(domain.trim() !== '' && { domain: domain.trim() }),
};
// shorten url in the background
const response: SuccessfulShortenStatusProperties | ApiErroredProperties = await messageUtil.send(
SHORTEN_URL,
apiBody
);
// re-enable submit button
setSubmitting(false);
if (!response.error) {
const {
data: { link },
} = response;
// show shortened url
setRequestProcessed({ error: false, message: link });
} else {
// errored
setRequestProcessed({ error: true, message: response.message });
}
},
displayName: 'PopupForm',
})(InnerForm);
export default PopupForm;

6
src/Popup/index.tsx Normal file
View File

@@ -0,0 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Popup from './Popup';
ReactDOM.render(<Popup />, document.getElementById('popup-root'));

64
src/Popup/styles.scss Normal file
View File

@@ -0,0 +1,64 @@
@import "../styles/fonts";
@import "../styles/reset";
@import "../styles/variables";
body {
color: $black;
}
#popup {
min-height: 300px;
min-width: 250px;
font-size: 1.125em;
text-align: center;
#popup__form {
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
div {
padding: 5px 10px;
width: 100%;
.error {
font-size: 11px;
padding: 2px 10px;
}
}
button {
padding: 7px 22px;
margin: 15px auto;
cursor: pointer;
background-color: lightblue;
}
}
}
#header {
padding: 5px;
display: flex;
justify-content: space-between;
align-items: center;
.logo__holder {
img {
width: 30px !important;
height: 30px !important;
}
}
.action__buttons--holder {
.icon {
vertical-align: middle;
border: none;
align-items: center;
justify-content: center;
border-radius: 100%;
background-color: transparent !important;
cursor: pointer;
}
}
}

6
src/api/index.ts Normal file
View File

@@ -0,0 +1,6 @@
import axios from 'axios';
export default axios.create({
// ToDo: get from local storage
baseURL: 'https://kutt.it',
});

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 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="trash-alt" class="svg-inline--fa fa-trash-alt fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="#888" d="M268 416h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12zM432 80h-82.41l-34-56.7A48 48 0 0 0 274.41 0H173.59a48 48 0 0 0-41.16 23.3L98.41 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16v336a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM171.84 50.91A6 6 0 0 1 177 48h94a6 6 0 0 1 5.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12z"></path></svg>

Before

Width:  |  Height:  |  Size: 731 B

View File

@@ -1 +0,0 @@
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="history" class="svg-inline--fa fa-history fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#444" d="M504 255.531c.253 136.64-111.18 248.372-247.82 248.468-59.015.042-113.223-20.53-155.822-54.911-11.077-8.94-11.905-25.541-1.839-35.607l11.267-11.267c8.609-8.609 22.353-9.551 31.891-1.984C173.062 425.135 212.781 440 256 440c101.705 0 184-82.311 184-184 0-101.705-82.311-184-184-184-48.814 0-93.149 18.969-126.068 49.932l50.754 50.754c10.08 10.08 2.941 27.314-11.313 27.314H24c-8.837 0-16-7.163-16-16V38.627c0-14.254 17.234-21.393 27.314-11.314l49.372 49.372C129.209 34.136 189.552 8 256 8c136.81 0 247.747 110.78 248 247.531zm-180.912 78.784l9.823-12.63c8.138-10.463 6.253-25.542-4.21-33.679L288 256.349V152c0-13.255-10.745-24-24-24h-16c-13.255 0-24 10.745-24 24v135.651l65.409 50.874c10.463 8.137 25.541 6.253 33.679-4.21z"></path></svg>

Before

Width:  |  Height:  |  Size: 947 B

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

@@ -0,0 +1,18 @@
import React from 'react';
type WrapperProperties = {
children: React.ReactChild;
};
const BodyWrapper: React.FC<WrapperProperties> = ({ children }) => {
// ToDo: get from props
const isLoading = false;
return (
<>
<div>{isLoading ? 'Loading...' : children}</div>
</>
);
};
export default BodyWrapper;

View File

@@ -0,0 +1,27 @@
import React from 'react';
import Settings from './Settings';
import Spinner from './Spinner';
const icons = {
settings: Settings,
spinner: Spinner,
};
export type Icons = keyof typeof icons;
type Props = {
name: Icons;
stroke?: string;
fill?: string;
hoverFill?: string;
hoverStroke?: string;
strokeWidth?: string;
className?: string;
};
const Icon: React.FC<Props> = ({ name, ...rest }) => {
return <div {...rest}>{React.createElement(icons[name])}</div>;
};
export default Icon;

View File

@@ -0,0 +1,17 @@
import React from 'react';
const Settings: React.FC = () => {
return (
<svg
viewBox="-2 -2 24 24"
width="32"
height="32"
preserveAspectRatio="xMinYMin"
className="cog_svg__jam cog_svg__jam-cog"
>
<path d="M20 8.163A2.106 2.106 0 0018.926 10c0 .789.433 1.476 1.074 1.837l-.717 2.406a2.105 2.105 0 00-2.218 3.058l-2.062 1.602A2.104 2.104 0 0011.633 20l-3.29-.008a2.104 2.104 0 00-3.362-1.094l-2.06-1.615A2.105 2.105 0 00.715 14.24L0 11.825A2.106 2.106 0 001.051 10C1.051 9.22.63 8.54 0 8.175L.715 5.76a2.105 2.105 0 002.207-3.043L4.98 1.102A2.104 2.104 0 008.342.008L11.634 0a2.104 2.104 0 003.37 1.097l2.06 1.603a2.105 2.105 0 002.218 3.058L20 8.162zM14.823 3.68c0-.063.002-.125.005-.188l-.08-.062a4.103 4.103 0 01-4.308-1.428l-.904.002a4.1 4.1 0 01-4.29 1.43l-.095.076A4.108 4.108 0 012.279 7.6a4.1 4.1 0 01.772 2.399c0 .882-.28 1.715-.772 2.4a4.108 4.108 0 012.872 4.09l.096.075a4.104 4.104 0 014.289 1.43l.904.002a4.1 4.1 0 014.307-1.428l.08-.062A4.108 4.108 0 0117.7 12.4a4.102 4.102 0 01-.773-2.4c0-.882.281-1.716.773-2.4a4.108 4.108 0 01-2.876-3.919zM10 14a4 4 0 110-8 4 4 0 010 8zm0-2a2 2 0 100-4 2 2 0 000 4z" />
</svg>
);
};
export default React.memo(Settings);

View File

@@ -0,0 +1,34 @@
import React from 'react';
import './styles.scss';
const Spinner: React.FC = () => {
return (
<>
<svg
id="spinner"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
stroke="currentColor"
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 @@
export { default } from './Icon';

View File

@@ -0,0 +1,13 @@
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
#spinner {
animation: spin 1s linear infinite;
}

View File

@@ -0,0 +1,84 @@
/* eslint-disable no-nested-ternary */
import React from 'react';
import { FieldProps } from 'formik';
import './styles.scss';
type SelectFieldProperties = {
options: SelectFieldOptionPropeties[];
label: string;
};
type SelectFieldOptionPropeties = {
option: string;
value: string;
disabled?: boolean | undefined;
};
export const SelectField: React.FC<SelectFieldProperties & FieldProps> = ({
options,
label,
field,
form: { touched, errors },
...props
}) => {
return (
<>
<label htmlFor={field.name}>{label}</label>
<div style={{ padding: '0px' }}>
<select {...field} {...props}>
{options.map(({ option, value, disabled = false }: SelectFieldOptionPropeties, index: number) => {
return (
<option value={value} disabled={disabled} key={index}>
{option}
</option>
);
})}
</select>
</div>
{touched[field.name] && errors[field.name] && <div className="error">{errors[field.name]}</div>}
</>
);
};
type TextFieldProperties = {
label: string;
};
export const TextField: React.FC<TextFieldProperties & FieldProps> = ({
label,
field,
form: { touched, errors },
...props
}) => {
return (
<>
<label htmlFor={field.name}>{label}</label>
<input {...field} {...props} />
{touched[field.name] && errors[field.name] && <div className="error">{errors[field.name]}</div>}
</>
);
};
type CheckBoxProperties = {
label: string;
};
export const CheckBox: React.FC<CheckBoxProperties & FieldProps> = ({ label, field, form, ...props }) => {
return (
<>
<label htmlFor={field.name}>{label}</label>
<input
type="checkbox"
checked={field.value}
onChange={(): void => {
form.setFieldValue(field.name, !field.value);
}}
{...field}
{...props}
/>
</>
);
};

View File

@@ -0,0 +1,24 @@
input {
height: 35px;
width: 100%;
display: flex;
position: relative;
margin: 0px;
border-radius: 100px;
border: 5px solid rgb(245, 245, 245);
}
select {
height: 35px;
padding: 10px 15px;
width: 100%;
display: flex;
position: relative;
margin: 0px;
border-radius: 100px;
background-color: rgb(245, 245, 245);
option {
background-color: rgb(245, 245, 245);
}
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
import './styles.scss';
import Icon from '../Icon';
const Loader: React.FC = props => {
return (
<div id="loader" {...props}>
<Icon name="spinner" />
</div>
);
};
export default Loader;

View File

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

View File

@@ -1,74 +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>History: Kutt</title>
<link rel="stylesheet" href="css/history.css" />
</head>
<body>
<section id="history">
<div class="container history__content--holder">
<div class="table__content--holder">
<div class="history__head--holder">
<h2>
Recent shortened links. (last 15 results)
</h2>
<div>
<a
href="#"
id="home__button"
target="_blank"
rel="noopener noreferrer nofollow"
>Dashboard</a
>
<a
href="#"
id="rate__button"
target="_blank"
rel="noopener noreferrer nofollow"
>Rate 5 stars</a
>
</div>
</div>
<table class="table__content--wrapper" id="URL_table">
<thead class="table__content--head">
<tr class="table__head--holder">
<th class="table__head--longURL">
Original URL
</th>
<th class="table__head--shortURL">
Short URL
</th>
<th class="table__head--clearAll">
<ul class="table__list--clearAll">
<li class="table__listItem--clear">
<button
class="table__clearAll--btn"
id="table__clearAll--btn"
>
Clear All
<img
class="selectDisable icon__img"
src="assets/delete.svg"
alt="Clear All"
/>
</button>
</li>
</ul>
</th>
</tr>
</thead>
<tbody class="table__content--body" id="delegation__element">
<!-- Inject children here -->
</tbody>
</table>
</div>
</div>
</section>
<script src="js/history.js"></script>
</body>
</html>

View File

@@ -41,7 +41,7 @@ const manifestInput = {
background: {
'__chrome|opera__persistent': false,
scripts: ['js/background.js'],
scripts: ['js/background.bundle.js'],
},
__chrome__minimum_chrome_version: '49',

View File

@@ -1,149 +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 noreferrer nofollow"
>Kutt</a
>
</div>
<div class="form__content--holder">
<form class="form__content">
<label class="api__key--label"
>API Key:
<a
class="api__label--text"
target="_blank"
rel="noopener noreferrer nofollow"
href="https://kutt.it/login"
>(Get one)
<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"
/>
<div>
<label class="password--label"
>Set Password
<span class="password__label--optional" style="font-size: 16px;"
>?
<span class="password__label--tooltiptext text-center">
Set Password for the Shortened URLs.<br />
(20 Char. Max)
</span>
</span>
</label>
<label
class="switch"
id="password__label--switch"
for="password__label--checkbox"
>
<input type="checkbox" id="password__label--checkbox" />
<div class="slider round"></div>
</label>
</div>
<div class="mb-2 d-none" id="pwd__holder">
<input
class="password--holder"
id="password--value"
type="password"
maxlength="20"
spellcheck="false"
/>
<span class="view__password--eye" id="view__password--eye">
SHOW
</span>
</div>
<div>
<label class="copy--label">Auto-copy URL to clipboard</label>
<label
class="switch"
id="autocopy__label--switch"
for="autocopy__label--checkbox"
>
<input type="checkbox" id="autocopy__label--checkbox" />
<div class="slider round"></div>
</label>
</div>
<div>
<label class="copy--label">Keep URL History</label>
<label
class="switch"
id="history__label--switch"
for="history__label--checkbox"
>
<input type="checkbox" id="history__label--checkbox" />
<div class="slider round"></div>
</label>
</div>
<div class="dev__mode--container">
<label class="customhost__mode--label"
>Custom Host
<span class="customhost__label--optional"
>(Advanced)
<span class="customhost__label--tooltiptext text-center">
Use extension for self&nbsp;-&nbsp;hosted Kutt.<br />
Paste the self hosted domain in the field.
(eg:&nbsp;https://mykutt.it)
</span>
</span>
</label>
<label
class="switch"
id="customhost__label--switch"
for="customhost__label--checkbox"
>
<input type="checkbox" id="customhost__label--checkbox" />
<div class="slider round"></div>
</label>
</div>
<div class="mb-2 d-none" id="customhost__holder">
<input
class="customhost__mode--holder text-center"
id="customhost__mode--value"
type="text"
placeholder="https://mykutt.it"
spellcheck="false"
/>
</div>
<button class="button__submit" id="button__submit" type="button">
Save
</button>
</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 noreferrer nofollow"
>GitHub</a
>
</div>
</div>
</section>
<script src="js/options.js"></script>
</body>
</html>

View File

@@ -1,94 +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">
<header class="header">
<div class="main__logo--holder">
<img
style="width: 22px; height: 22px;"
class="main__logo selectDisable"
src="assets/logo.png"
/>
</div>
<div class="main__list--holder">
<a
class="list__button"
href="options.html"
target="_blank"
rel="noopener noreferrer nofollow"
title="Options"
>
<img
style="width: 16px; height: 16px;"
class="button__icon selectDisable"
src="assets/settings.svg"
/></a>
<a
class="list__button"
href="history.html"
target="_blank"
rel="noopener noreferrer nofollow"
title="History"
>
<img
style="width: 16px; height: 16px;"
class="button__icon selectDisable"
src="assets/history.svg"
/></a>
</div>
</header>
<div class="content__holder">
<div class="url__content--holder text-center">
<div class="url__content--url">
<div class="v-none" id="copy__alert">Copied to clipboard!</div>
<h4 id="url__content-inner">Shortening...</h4>
</div>
<ul class="buttons__content--holder d-none">
<li
class="copy__content--holder"
id="button__copy--holder"
title="Copy"
>
<img
style="width: 13px; height: 13px;"
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
style="width: 13px; height: 13px;"
id="button__qrcode"
class="selectDisable"
src="assets/qrcode.svg"
alt="QR Code"
/>
</li>
</ul>
</div>
<div class="qrcode__content--holder selectDisable 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,87 +0,0 @@
import axios from 'axios';
import browser from 'webextension-polyfill';
import { KUTT_IT_DEFAULT_DOMAIN } from './constants';
// Shorten url
const shortenUrl = async (API_KEY, urlToShorten, password) => {
let API_HOST = KUTT_IT_DEFAULT_DOMAIN;
try {
const { host, userOptions } = await browser.storage.local.get(['host', 'userOptions']);
// eslint-disable-next-line no-prototype-builtins
if (userOptions.hasOwnProperty('devMode') && userOptions.devMode) {
API_HOST = host;
}
// else use default host
} catch (err) {
API_HOST = KUTT_IT_DEFAULT_DOMAIN;
}
// shorten function
try {
const {
data: { shortUrl },
} = await axios({
method: 'POST',
timeout: 20000,
url: `${API_HOST}/api/url/submit`,
headers: {
'X-API-Key': API_KEY,
},
data: {
target: urlToShorten,
password,
},
});
return shortUrl;
} catch (e) {
// time out
if (e.code === 'ECONNABORTED') {
return 504;
}
// return status code
if (e.response) {
return e.response.status;
}
}
};
// Calling function
browser.runtime.onMessage.addListener(async (request, sender, response) => {
// shorten request
if (request.msg === 'shorten') {
const { API_KEY, pageUrl, password } = request;
return shortenUrl(API_KEY, pageUrl, password);
}
// store urls to history
if (request.msg === 'store') {
const { curURLCollection } = request;
const { curURLPair } = request;
// find & remove duplicates
const noDuplicateArray = curURLCollection.filter(el => {
return el.longUrl !== curURLPair.longUrl;
});
const count = noDuplicateArray.length;
// delete first pair if size exceeds 15
if (count >= 15) {
noDuplicateArray.shift();
}
// push to the array
noDuplicateArray.push(curURLPair);
// save to local storage
await browser.storage.local.set({
URL_array: noDuplicateArray,
});
}
});

View File

@@ -1,18 +0,0 @@
/* eslint-disable no-multi-assign */
const $ = document.querySelector.bind(document);
const $$ = document.querySelectorAll.bind(document);
Node.prototype.on = window.on = function(name, fn) {
this.addEventListener(name, fn);
};
// eslint-disable-next-line no-proto
NodeList.prototype.__proto__ = Array.prototype;
NodeList.prototype.on = NodeList.prototype.addEventListener = function(name, fn) {
this.forEach(function(elem, i) {
elem.on(name, fn);
});
};
export { $, $$ };

View File

@@ -1,87 +0,0 @@
/* eslint-disable camelcase */
export const KUTT_IT_DEFAULT_DOMAIN = 'https://kutt.it';
export const FIREFOX = 'firefox';
export const OPERA = 'opera';
export const CHROME = 'chrome';
// popup page
export const QRCODE_IMAGE_NODE = '#qr_code';
export const URL_HOLDER = '#url__content-inner';
export const BUTTONS_GROUP = '.buttons__content--holder';
export const COPY_BUTTON = '#button__copy--holder';
export const COPIED_ALERT_HOLDER = '#copy__alert';
export const QRCODE_HOLDER = '.qrcode__content--holder';
export const QRCODE_BUTTON = '#button__qrcode--holder';
export const QR_EXTERNAL_API_URL = 'https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=';
// options page
export const PASSWORD_HOLDER = '#pwd__holder';
export const CUSTOM_HOST_URL_HOLDER = '#customhost__holder';
export const SAVE_BUTTON = '#button__submit';
export const PASSWORD_INPUT = '#password--value';
export const CUSTOM_HOST_URL_INPUT = '#customhost__mode--value';
export const API_KEY_INPUT = '#api__key--value';
export const PASSWORD_VIEW_TOGGLER = '#view__password--eye';
export const PASSWORD_INPUT_TOGGLE_SWITCH = '#password__label--switch';
export const CUSTOM_HOST_INPUT_TOGGLE_SWITCH = '#customhost__label--switch';
export const PASSWORD_OPTION_CHECKBOX = '#password__label--checkbox';
export const CUSTOM_HOST_OPTION_CHECKBOX = '#customhost__label--checkbox';
export const SAVE_HISTORY_OPTION_CHECKBOX = '#history__label--checkbox';
export const AUTOCOPY_OPTION_CHECKBOX = '#autocopy__label--checkbox';
// history page
export const CLEAR_HISTORY_BUTTON = '#table__clearAll--btn';
export const ALERT_COPIED_HOLDER = '#flash_copy';
export const HISTORY_VIEW_TABLE = '.table__content--holder';
export const RATE_NOW_BUTTON = '#rate__button';
export const DASHBOARD_BUTTON = '#home__button';
export const HISTORY_VIEW_TABLE_PARENT_NODE = '#delegation__element';
export const CHROME_STORE_LINK =
'https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd/reviews';
export const FIREFOX_STORE_LINK = 'https://addons.mozilla.org/en-US/firefox/addon/kutt/reviews/';
export const COPY_BUTTON_ID = 'copy';
export const QRCODE_BUTTON_ID = 'qrcode';
export const QRCODE_POPUP_CLOSE_BUTTON_ID = 'close__btn';
export const NO_URLS_TO_SHOW = '<h2 class="py-2 table-inner">No Shortened URLs</h2>';
export const COPIED_TO_CLIPBOARD = '<div class="table_body--flashCopy" id="flash_copy">Copied to clipboard!</div>';
export const FAILED_TO_COPY = '<div class="table_body--flashCopy" id="flash_copy">Error while Copying!!</div>';
export const QRCODE_POPUP_NODE = '#qrcode__template';
export const QRCODE_POPUP_NODE_TEMPLATE = `
<div class="table__qrcodePopup--div" id="qrcode__template">
<div class="table__qrcode--popup">
<div class="table__qrcode--holder">
<img id="table__qrcode" src="%qrcodeLink%" alt="QRCode" />
</div>
<div class="table__closebtn--holder">
<button type="button" class="table__closebtn--inner" id="close__btn-%num%">Close</button>
</div>
</div>
</div>
`;
export const HISTORY_TABLE_ITEM_HTML = `
<tr class="table__body--holder" id="table__body-%num%">
<td class="table__body--original">
<a href="%longLink%" class="table__body--originalURL" target="_blank" rel="noopener noreferrer nofollow">%longLink%</a>
</td>
<td class="table__body--shortened" id="table__shortened-%num%">
<div class="table__body--shortenBody">
<a href="%shortLink%" id="shortUrl-%num%" class="table__body--shortenURL" target="_blank" rel="noopener noreferrer nofollow">%shortLink%</a>
</div>
</td>
<td class="table__body--functionBtns">
<div class="table__body--btnHolder" id="btns-%num%">
<button type="button" class="table__body--copy" id="copy-%num%" title="Copy">
<img class="selectDisable icon__img" src="assets/copy.svg" alt="copy" />
</button>
<button type="button" class="table__body--qrcode" id="qrcode-%num%" title="QR Code">
<img class="selectDisable icon__img" src="assets/qrcode.svg" alt="QR Code" />
</button>
</div>
</td>
</tr>
`;

View File

@@ -1,237 +0,0 @@
/* eslint-disable prefer-destructuring */
/* eslint-disable camelcase */
import browser from 'webextension-polyfill';
import qr from 'qrcode';
import {
HISTORY_TABLE_ITEM_HTML,
CLEAR_HISTORY_BUTTON,
HISTORY_VIEW_TABLE,
RATE_NOW_BUTTON,
DASHBOARD_BUTTON,
HISTORY_VIEW_TABLE_PARENT_NODE,
CHROME_STORE_LINK,
FIREFOX_STORE_LINK,
QR_EXTERNAL_API_URL,
KUTT_IT_DEFAULT_DOMAIN,
FIREFOX,
OPERA,
CHROME,
ALERT_COPIED_HOLDER,
NO_URLS_TO_SHOW,
COPIED_TO_CLIPBOARD,
FAILED_TO_COPY,
QRCODE_POPUP_NODE_TEMPLATE,
QRCODE_POPUP_NODE,
COPY_BUTTON_ID,
QRCODE_BUTTON_ID,
QRCODE_POPUP_CLOSE_BUTTON_ID,
} from './constants';
import { $ } from './bling';
/**
* Identify Browser
*/
const getBrowserInfo = () => {
// Chrome 1+
const isChrome = !!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime);
// Firefox 1.0+
const isFirefox = typeof InstallTrigger !== 'undefined';
// Opera 8.0+
// eslint-disable-next-line no-undef
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
if (isFirefox) {
return FIREFOX;
}
if (isOpera) {
return OPERA;
}
return CHROME;
};
/**
* Update Store Link
*/
const updateRatingButton = () => {
const browserName = getBrowserInfo();
switch (browserName) {
case CHROME:
case OPERA: {
$(RATE_NOW_BUTTON).setAttribute('href', CHROME_STORE_LINK);
break;
}
case FIREFOX: {
$(RATE_NOW_BUTTON).setAttribute('href', FIREFOX_STORE_LINK);
break;
}
default:
break;
}
};
/**
* Update Home Page URL
*/
document.on('DOMContentLoaded', async () => {
let updatedHTML;
const {
userOptions: { keepHistory, devMode },
URL_array,
host,
} = await browser.storage.local.get(['userOptions', 'URL_array', 'host']);
if (keepHistory) {
const count = URL_array.length;
// update DOM
if (count > 0) {
let pass = 0;
for (const el of URL_array) {
// Regular Expression Based Implementation
updatedHTML = HISTORY_TABLE_ITEM_HTML.replace(/%longLink%/g, el.longUrl);
pass += 1;
updatedHTML = updatedHTML.replace(/%num%/g, pass);
updatedHTML = updatedHTML.replace(/%shortLink%/g, el.shortUrl);
// inject to DOM
$(HISTORY_VIEW_TABLE_PARENT_NODE).insertAdjacentHTML('afterbegin', updatedHTML);
}
} else {
$(CLEAR_HISTORY_BUTTON).style.display = 'none';
$(HISTORY_VIEW_TABLE_PARENT_NODE).insertAdjacentHTML('afterbegin', NO_URLS_TO_SHOW);
}
// update review link
updateRatingButton();
// update home page url
const hostHomeUrl = devMode ? host : KUTT_IT_DEFAULT_DOMAIN;
$(DASHBOARD_BUTTON).setAttribute('href', hostHomeUrl);
} else {
alert('Enable History from Options Page');
// open options page in new tab
browser.runtime.openOptionsPage();
}
});
/**
* Clear all history
*/
$(CLEAR_HISTORY_BUTTON).on('click', async () => {
await browser.storage.local.set({
URL_array: [],
});
$(HISTORY_VIEW_TABLE_PARENT_NODE).parentNode.removeChild($(HISTORY_VIEW_TABLE_PARENT_NODE));
$(CLEAR_HISTORY_BUTTON).style.display = 'none';
$(HISTORY_VIEW_TABLE).insertAdjacentHTML('beforeend', NO_URLS_TO_SHOW);
});
/**
* Handle Buttons Click Actions
*/
const buttonAction = async (type, id) => {
const flashCopyAlert = flashHTML => {
$(`#table__shortened-${id}`).insertAdjacentHTML('afterbegin', flashHTML);
setTimeout(() => {
$(ALERT_COPIED_HOLDER).parentNode.removeChild($(ALERT_COPIED_HOLDER));
}, 1300);
};
// copy button
if (type === COPY_BUTTON_ID) {
const shortLink = $(`#shortUrl-${id}`).textContent;
try {
const el = document.createElement('textarea');
el.value = shortLink;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
el.select();
document.execCommand('copy');
document.body.removeChild(el);
if (selected) {
document.getSelection().removeAllRanges();
document.getSelection().addRange(selected);
}
flashCopyAlert(COPIED_TO_CLIPBOARD);
} catch (error) {
flashCopyAlert(FAILED_TO_COPY);
}
}
// generate QRCode
else if (type === QRCODE_BUTTON_ID) {
let updatedHTML;
// 1. get short link
const shortUrl = $(`#shortUrl-${id}`).textContent;
// 2. generate qrcode
try {
const qrcodeURL = await qr.toDataURL(shortUrl);
// 3. display popup menu with link
updatedHTML = QRCODE_POPUP_NODE_TEMPLATE.replace('%qrcodeLink%', qrcodeURL);
updatedHTML = updatedHTML.replace('%num%', id);
$(`#btns-${id}`).insertAdjacentHTML('afterend', updatedHTML);
} catch (err) {
// fetch qrcode from http://goqr.me
updatedHTML = QRCODE_POPUP_NODE_TEMPLATE.replace('%qrcodeLink%', `${QR_EXTERNAL_API_URL}${shortUrl}`);
$(`#btns-${id}`).insertAdjacentHTML('afterend', updatedHTML);
}
} else if (type === QRCODE_POPUP_CLOSE_BUTTON_ID) {
$(QRCODE_POPUP_NODE).parentNode.removeChild($(QRCODE_POPUP_NODE));
}
};
/**
* get the delegation id (child node)
*/
const getButtonDetails = e => {
let splitId;
let type;
let id;
const eventId = e.target.id;
if (eventId) {
splitId = eventId.split('-');
type = splitId[0];
id = parseInt(splitId[1]);
// perform action
buttonAction(type, id);
}
};
/**
* Button Action (qrcode / copy)
*/
$(HISTORY_VIEW_TABLE_PARENT_NODE).on('click', getButtonDetails);
/**
* prevent enter key press
*/
document.on('keypress', e => {
const keyCode = e.which || e.keyCode;
if (keyCode === 13) {
e.preventDefault();
}
});

View File

@@ -1,173 +0,0 @@
/* eslint-disable no-use-before-define */
/* eslint-disable camelcase */
import browser from 'webextension-polyfill';
import {
PASSWORD_HOLDER,
CUSTOM_HOST_URL_HOLDER,
SAVE_BUTTON,
PASSWORD_INPUT,
CUSTOM_HOST_URL_INPUT,
API_KEY_INPUT,
PASSWORD_VIEW_TOGGLER,
PASSWORD_INPUT_TOGGLE_SWITCH,
CUSTOM_HOST_INPUT_TOGGLE_SWITCH,
PASSWORD_OPTION_CHECKBOX,
CUSTOM_HOST_OPTION_CHECKBOX,
SAVE_HISTORY_OPTION_CHECKBOX,
AUTOCOPY_OPTION_CHECKBOX,
} from './constants';
import { $ } from './bling';
document.on('DOMContentLoaded', async () => {
// get values from localstorage
let { key, pwd, userOptions, host } = await browser.storage.local.get(['key', 'pwd', 'userOptions', 'host']);
// don't use toString() as it will fail for `undefined`
const API_KEY = `${key}`;
if (API_KEY === 'undefined') {
$(API_KEY_INPUT).value = '';
} else {
$(API_KEY_INPUT).value = API_KEY;
// password holder
$(PASSWORD_OPTION_CHECKBOX).checked = userOptions.pwdForUrls;
// if disabled -> delete saved password
if (!userOptions.pwdForUrls) {
pwd = '';
}
$(PASSWORD_INPUT).value = pwd;
toggleInputVisibility(userOptions.pwdForUrls, PASSWORD_HOLDER);
// dev mode holder
$(CUSTOM_HOST_OPTION_CHECKBOX).checked = userOptions.devMode;
// if disabled -> reset to default host
if (!userOptions.devMode) {
host = '';
}
$(CUSTOM_HOST_URL_INPUT).value = host;
toggleInputVisibility(userOptions.devMode, CUSTOM_HOST_URL_HOLDER);
}
$(AUTOCOPY_OPTION_CHECKBOX).checked = userOptions.autoCopy;
$(SAVE_HISTORY_OPTION_CHECKBOX).checked = userOptions.keepHistory;
});
// Store Data and Alert message
const saveData = async () => {
let password = $(PASSWORD_INPUT).value;
let API_HOST = $(CUSTOM_HOST_URL_INPUT).value;
const API_KEY = $(API_KEY_INPUT).value;
let devMode = $(CUSTOM_HOST_OPTION_CHECKBOX).checked;
let pwdForUrls = $(PASSWORD_OPTION_CHECKBOX).checked;
const autoCopy = $(AUTOCOPY_OPTION_CHECKBOX).checked;
const keepHistory = $(SAVE_HISTORY_OPTION_CHECKBOX).checked;
if (password === '') {
pwdForUrls = false;
}
if (!pwdForUrls) {
password = '';
}
if (API_HOST === '') {
devMode = false;
} else if (API_HOST.endsWith('/')) {
API_HOST = API_HOST.slice(0, -1);
}
if (!devMode) {
API_HOST = '';
}
const userOptions = {
pwdForUrls,
autoCopy,
devMode,
keepHistory,
};
// store value locally
await browser.storage.local.set({
key: API_KEY,
pwd: password,
host: API_HOST,
URL_array: [],
userOptions,
});
$(SAVE_BUTTON).textContent = 'Saved';
setTimeout(async () => {
$(SAVE_BUTTON).textContent = 'Save';
// close current tab
const tabInfo = await browser.tabs.getCurrent();
browser.tabs.remove(tabInfo.id);
}, 1250);
};
/**
* Handle submit button click
*/
$(SAVE_BUTTON).on('click', saveData);
/**
* Handle enter-key press
*/
document.on('keypress', e => {
if (e.keyCode === 13) {
saveData();
}
});
/**
* Toggle Password View
*/
$(PASSWORD_VIEW_TOGGLER).on('click', () => {
const element = $(PASSWORD_INPUT);
if (element.type === 'password') {
element.type = 'text';
$(PASSWORD_VIEW_TOGGLER).textContent = 'HIDE';
} else {
element.type = 'password';
$(PASSWORD_VIEW_TOGGLER).textContent = 'SHOW';
}
});
/**
* Toggle Element Visibility
*/
function toggleInputVisibility(checked, el) {
if (checked) {
$(el).classList.remove('d-none');
} else {
$(el).classList.add('d-none');
}
}
/**
* Password Enable/Disable Switch
*/
$(PASSWORD_INPUT_TOGGLE_SWITCH).on('click', () => {
const { checked } = $(PASSWORD_OPTION_CHECKBOX);
toggleInputVisibility(checked, PASSWORD_HOLDER);
});
/**
* Customhost Mode Enable/Disable Switch
*/
$(CUSTOM_HOST_INPUT_TOGGLE_SWITCH).on('click', () => {
const { checked } = $(CUSTOM_HOST_OPTION_CHECKBOX);
toggleInputVisibility(checked, CUSTOM_HOST_URL_HOLDER);
});

View File

@@ -1,252 +0,0 @@
/* eslint-disable camelcase */
import browser from 'webextension-polyfill';
import qr from 'qrcode';
import {
QRCODE_IMAGE_NODE,
URL_HOLDER,
BUTTONS_GROUP,
COPY_BUTTON,
COPIED_ALERT_HOLDER,
QRCODE_BUTTON,
QR_EXTERNAL_API_URL,
QRCODE_HOLDER,
} from './constants';
import { $ } from './bling';
let shortUrl;
let longUrl;
let API_KEY;
let password;
let validUrl = '';
/**
* DOM Message Update function
*/
const updateDOMContent = value => {
$(URL_HOLDER).textContent = value;
};
/**
* Trigger Opening Options Page
*/
const openOptionsPage = () => {
setTimeout(() => {
browser.runtime.openOptionsPage();
}, 900);
};
/**
* Show / Hide Components
*
* @param {String} element ID or class
*/
const toggleContentVisibility = element => {
$(element).classList.toggle('d-none');
};
/**
* Show / Hide copied text alert
*/
const toggleCopyAlert = () => {
$(COPIED_ALERT_HOLDER).classList.toggle('v-none');
};
/**
* QR Code generator
*
* @param {String} url
*/
const generateQRCode = async sourceUrl => {
try {
$(QRCODE_IMAGE_NODE).src = await qr.toDataURL(sourceUrl);
} catch (err) {
// fetch qrcode from http://goqr.me api
$(QRCODE_IMAGE_NODE).src = `${QR_EXTERNAL_API_URL}${sourceUrl}`;
}
};
/**
* Copy Link to Clipboard
*/
const copyLinkToClipboard = () => {
// https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f
try {
$(COPIED_ALERT_HOLDER).textContent = 'Copied to clipboard!';
const el = document.createElement('textarea');
el.value = shortUrl;
el.setAttribute('readonly', '');
el.style.position = 'absolute';
el.style.left = '-9999px';
document.body.appendChild(el);
const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
el.select();
document.execCommand('copy');
document.body.removeChild(el);
if (selected) {
document.getSelection().removeAllRanges();
document.getSelection().addRange(selected);
}
toggleCopyAlert();
setTimeout(() => {
toggleCopyAlert();
}, 1300);
} catch (error) {
$(COPIED_ALERT_HOLDER).textContent = 'Error while Copying!';
toggleCopyAlert();
setTimeout(() => {
toggleCopyAlert();
}, 1300);
}
};
/**
* Add URL to localstorage array
*/
const addToHistory = async curURLPair => {
const { URL_array } = await browser.storage.local.get(['URL_array']);
// store to localstorage
await browser.runtime.sendMessage({
msg: 'store',
curURLPair,
curURLCollection: URL_array,
});
};
/**
* Handle User Preferred Actions (autoCopy/keepHistory)
*/
const doUserSetActions = async () => {
const { userOptions } = await browser.storage.local.get(['userOptions']);
const { keepHistory, autoCopy } = userOptions;
if (autoCopy) {
setTimeout(() => {
copyLinkToClipboard();
}, 500);
}
if (keepHistory) {
const curURLPair = {
longUrl,
shortUrl,
};
addToHistory(curURLPair);
}
};
/**
* Handle copying on button click
*/
$(COPY_BUTTON).on('click', () => {
return copyLinkToClipboard();
});
/**
* Show / Hide QRCode on button click
*/
$(QRCODE_BUTTON).on('click', () => {
toggleContentVisibility(QRCODE_HOLDER);
});
/**
* Driver Function
*/
document.addEventListener('DOMContentLoaded', async () => {
const tabs = await browser.tabs.query({
active: true,
lastFocusedWindow: true,
});
// extract page url
longUrl = tabs.length && tabs[0].url;
// validate url
if (longUrl) {
validUrl = longUrl.startsWith('http');
}
// Get API Key / Password from localstorage
const { key, pwd } = await browser.storage.local.get(['key', 'pwd']);
API_KEY = key;
password = pwd;
if (validUrl && API_KEY !== '' && API_KEY !== undefined) {
/**
* Initialize url shortening (send message to background.js)
*/
const response = await browser.runtime.sendMessage({
msg: 'shorten',
API_KEY,
pageUrl: longUrl,
password,
});
// eslint-disable-next-line no-restricted-globals
if (!isNaN(response)) {
// status codes
switch (response) {
case 429:
updateDOMContent('API Limit Exceeded!');
break;
case 401:
updateDOMContent('Invalid API Key');
openOptionsPage();
break;
case 504:
updateDOMContent('Time-out!');
break;
default:
updateDOMContent('Some error occured');
break;
}
}
// got valid response
else if (response) {
shortUrl = response;
// show shortened kutt url
updateDOMContent(shortUrl);
// Show action buttons
toggleContentVisibility(BUTTONS_GROUP);
// Generate QR Code
generateQRCode(shortUrl);
// perform user-set actions
doUserSetActions();
}
// all test-cases fail
else {
updateDOMContent('Invalid Response!');
}
}
// no API key set
else if (API_KEY === '' || API_KEY === undefined) {
updateDOMContent('Set API Key in Options!');
const defaultOptions = {
pwdForUrls: false,
autoCopy: false,
keepHistory: true,
devMode: false,
};
// set defaults
await browser.storage.local.set({
userOptions: defaultOptions,
URL_array: [],
});
openOptionsPage();
}
// invalid url
else if (!validUrl) {
updateDOMContent('Not a Valid URL!!');
} else {
updateDOMContent('Some error occured');
}
});

0
src/styles/_fonts.scss Normal file
View File

43
src/styles/_reset.scss Normal file
View File

@@ -0,0 +1,43 @@
// forked from Normalize.css, Reboot.css, Sanitize.css, and Untouched.css
*,
*:before,
*:after {
box-sizing: border-box;
}
*:focus {
outline: 0;
}
ol,
ul {
list-style-type: none;
}
* {
margin: 0;
padding: 0;
border: 0;
outline: 0;
}
body {
overflow-x: hidden;
}
a:link {
text-decoration: none;
}
input {
word-spacing: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
text-rendering: auto;
cursor: text;
margin: 0em;
padding: 1px 0px;
border-width: 2px;
}

View File

@@ -0,0 +1,24 @@
// **** colors ****
$black: #111111;
$light-black: #0f0f0f;
$grey-white: #f3f3f3;
$white: #ffffff;
// **** fonts ****
$font-nunito: "Nunito", sans-serif;
// font weights
$thin: 100;
$exlight: 200;
$light: 300;
$regular: 400;
$medium: 500;
$semibold: 600;
$bold: 700;
$exbold: 800;
$exblack: 900;
// **** other variables ****
.d-none {
display: none !important;
}

View File

@@ -1 +0,0 @@
@import url("https://fonts.googleapis.com/css?family=Nunito:400,600");

View File

@@ -1,31 +0,0 @@
// Manually forked from Normalize.css, Reboot.css, Sanitize.css, and Untouched.css
*,
*:before,
*:after {
box-sizing: border-box;
}
*:focus {
outline: 0;
}
ol,
ul {
list-style-type: none;
}
* {
margin: 0;
padding: 0;
border: 0;
outline: 0;
}
body {
overflow-x: hidden;
}
a:link {
text-decoration: none;
}

View File

@@ -1,117 +0,0 @@
// Colors
$color-black: #111111;
$color-light-black: #0f0f0f;
$color-shadow-black: rgba(50, 50, 50, 0.1);
$color-shadow-light: rgba(100, 100, 100, 0.1);
$color-null-black: rgba(0, 0, 0, 0);
$color-grey-white: #f3f3f3;
$color-white: #ffffff;
$color-light: #fcfcfc;
$color-white-less: #cccccc;
$color-border-white: #f5f5f5;
$color-less-white: #dedede;
$color-light-grey: #555;
$color-medium-grey: #444444;
$color-dark-grey: #333;
$color-light-azure: rgba(66, 165, 245, 0.5);
$color-light-blue: #4d5bfa;
$color-grad-light-blue: #42a5f5;
$color-grad-dark-blue: #2979ff;
$font-nunito: "Nunito", sans-serif;
$regular: 400;
$bold: 600;
.d-none {
display: none !important;
}
.v-none {
visibility: hidden !important;
}
.text-center {
text-align: center;
}
.mt-3 {
margin-top: 3em;
}
.mb-2 {
margin-bottom: 2em;
}
.my-2 {
margin-top: 1em;
margin-bottom: 2em;
}
.py-2 {
padding: 1em 24px;
}
.table-inner {
border-radius: 0 0 12px 12px;
box-shadow: rgba(50, 50, 50, 0.2) 0px 6px 30px;
background-color: white;
}
.selectDisable {
user-select: none;
}
body {
font-family: $font-nunito;
}
.icon__img {
width: 12px;
height: 12px;
}
/* toggle switch */
.switch {
height: 23px;
width: 49px;
margin-right: 10px;
float: right;
position: relative;
input {
display: none;
&:checked + .slider {
background-color: $color-grad-dark-blue;
}
&:checked + .slider:before {
transform: translateX(26px);
}
}
.slider {
background-color: $color-white-less;
bottom: 0;
cursor: pointer;
left: 0;
position: absolute;
right: 0;
top: 0;
transition: 0.4s;
&::before {
height: 15px;
width: 15px;
background-color: $color-white;
bottom: 4px;
content: "";
left: 4px;
position: absolute;
transition: 0.4s;
}
}
}
.slider.round {
border-radius: 34px;
&::before {
border-radius: 50%;
}
}

View File

@@ -1,334 +0,0 @@
@import "base/fonts";
@import "base/reset";
@import "base/variables";
body {
color: $color-black;
background-color: $color-grey-white;
}
button > * {
pointer-events: none;
}
#history {
.history__content--holder {
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
flex-direction: column;
box-sizing: border-box;
flex: 0 0 auto;
.table__content--holder {
width: 1200px;
max-width: 95%;
display: flex;
flex-direction: column;
margin: 40px 0px 120px;
.history__head--holder {
display: flex;
align-items: center;
justify-content: space-between;
h2 {
font-size: 24px;
margin: 0.83em 0;
}
a {
border-bottom: 1px solid;
margin-right: 10px;
padding-bottom: 1px;
font-size: 18px;
}
}
.table__content--wrapper {
display: flex;
flex-direction: column;
background-color: white;
box-shadow: rgba(50, 50, 50, 0.2) 0px 6px 30px;
flex: 1 1 auto;
border-radius: 12px;
.table__content--head {
display: flex;
flex-direction: column;
background-color: rgb(241, 241, 241);
border-top-right-radius: 12px;
border-top-left-radius: 12px;
flex: 1 1 auto;
.table__head--holder {
display: flex;
justify-content: space-between;
flex: 1 1 auto;
padding: 0px 24px;
border-bottom: 1px solid rgb(234, 234, 234);
th {
position: relative;
display: flex;
align-items: center;
padding: 16px 0px;
}
.table__head--longURL {
justify-content: flex-start;
align-items: center;
flex: 2 2 0px;
font-size: 16px;
line-height: 1.45;
}
.table__head--shortURL {
display: flex;
justify-content: flex-start;
align-items: center;
flex: 1 1 0px;
font-size: 16px;
line-height: 1.45;
}
.table__head--clearAll {
display: flex;
position: relative;
align-items: center;
padding: 16px 0px;
.table__list--clearAll {
.table__listItem--clear {
.table__clearAll--btn {
color: #111;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
height: 26px;
box-shadow: rgba(100, 100, 100, 0.1) 0px 2px 4px;
background-color: rgb(222, 222, 222);
cursor: pointer;
width: auto;
margin: 0px 2px 0px 12px;
border-width: initial;
border-style: none;
border-color: initial;
border-image: initial;
outline: none;
transition: all 0.2s ease-out 0s;
padding: 0px 12px;
border-radius: 100px;
img {
margin: 1px 0px 3px 6px;
}
&:hover {
transform: translateY(-3px);
}
}
}
}
}
}
}
.table__content--body {
display: flex;
flex-direction: column;
flex: 1 1 auto;
.table__body--holder {
display: flex;
justify-content: space-between;
flex: 1 1 auto;
padding: 0px 24px;
border-bottom: 1px solid rgb(234, 234, 234);
td {
position: relative;
display: flex;
align-items: center;
padding: 16px 0px;
}
.table__body--original {
white-space: nowrap;
overflow: hidden;
flex: 2 2 0px;
position: relative;
.table__body--originalURL {
color: rgb(33, 150, 243);
box-sizing: border-box;
text-decoration: none;
border-bottom: 1px dotted transparent;
transition: all 0.2s ease-out 0s;
font-size: 16px;
line-height: 1.45;
&:hover {
border-bottom: 1px dotted black;
}
}
&::after {
content: "";
position: absolute;
right: 0px;
top: 0px;
height: 100%;
width: 56px;
background: linear-gradient(to left, white, white, transparent);
}
}
.table__body--shortened {
white-space: nowrap;
flex: 1 1 23px;
overflow: hidden;
position: relative;
&::after {
content: "";
position: absolute;
right: 0px;
top: 0px;
height: 100%;
width: 56px;
background: linear-gradient(to left, white, white, transparent);
}
.table_body--flashCopy {
position: absolute;
left: 0px;
top: 0px;
color: green;
font-size: 11px;
}
.table__body--shortenBody {
display: flex;
align-items: center;
.table__body--shortenURL {
color: rgb(33, 150, 243);
box-sizing: border-box;
text-decoration: none;
border-bottom: 1px dotted transparent;
transition: all 0.2s ease-out 0s;
font-size: 16px;
line-height: 1.45;
&:hover {
border-bottom: 1px dotted black;
}
}
}
}
.table__body--functionBtns {
.table__body--btnHolder {
display: flex;
justify-content: flex-end;
align-items: center;
.table__body--qrcode,
.table__body--copy {
margin: 0px 2px 0px 12px;
display: flex;
justify-content: center;
align-items: center;
width: 26px;
height: 26px;
box-shadow: rgba(100, 100, 100, 0.1) 0px 2px 4px;
background-color: rgb(222, 222, 222);
cursor: pointer;
margin: 0px 12px 0px 2px;
padding: 0px;
border-width: initial;
border-style: none;
border-color: initial;
border-image: initial;
outline: none;
border-radius: 100%;
transition: all 0.2s ease-out 0s;
&:hover {
transform: translateY(-3px);
}
}
}
.table__qrcodePopup--div {
position: fixed;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(50, 50, 50, 0.8);
z-index: 1000;
.table__qrcode--popup {
text-align: center;
background-color: white;
padding: 48px 64px;
border-radius: 8px;
.table__qrcode--holder {
#table__qrcode {
height: 196px;
width: 196px;
}
}
.table__closebtn--holder {
display: flex;
justify-content: center;
margin-top: 40px;
.table__closebtn--inner {
margin: 0px 16px;
position: relative;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: normal;
text-align: center;
line-height: 1;
word-break: keep-all;
cursor: pointer;
color: black;
box-shadow: rgba(160, 160, 160, 0.5) 0px 5px 6px;
padding: 0px 32px;
border-width: initial;
border-style: none;
border-color: initial;
border-image: initial;
border-radius: 100px;
transition: all 0.4s ease-out 0s;
overflow: hidden;
background: linear-gradient(
to right,
rgb(224, 224, 224),
rgb(189, 189, 189)
);
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -1,206 +0,0 @@
@import "base/fonts";
@import "base/reset";
@import "base/variables";
body {
color: $color-black;
background-color: $color-grey-white;
}
#options {
display: flex;
justify-content: center;
height: 100vh;
.options__content--holder {
padding-top: 5em;
display: flex;
flex-direction: column;
align-items: center;
.head__content--holder {
display: flex;
align-items: center;
margin-bottom: 24px;
.head__content--logo {
width: 38px;
height: 38px;
}
.head__content--title {
color: $color-black;
font-weight: $bold;
padding: 0;
margin: 0 0 0 0.4em;
font-size: 34px;
}
}
.form__content--holder {
margin-top: 3em;
.form__content {
padding: 0px 100px 40px;
width: 600px;
.api__key--label,
.password--label,
.copy--label,
.customhost__mode--label {
font-size: 16px;
display: inline-block;
margin-bottom: 0.8em;
}
.password--label,
.api__key--label,
.customhost__mode--label {
.password__label--optional,
.customhost__label--optional,
.api__label--text {
font-size: 13px;
letter-spacing: 1px;
cursor: default;
position: relative;
display: inline-block;
border-bottom: 1px dotted black;
&:hover .password__label--tooltiptext,
&:hover .api__label--tooltiptext,
&:hover .customhost__label--tooltiptext {
visibility: visible;
opacity: 1;
}
.password__label--tooltiptext,
.api__label--tooltiptext,
.customhost__label--tooltiptext {
cursor: pointer;
visibility: hidden;
font-size: 13px;
width: 180px;
line-height: 1.5;
letter-spacing: 1px;
background-color: $color-light-grey;
color: $color-white;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -90px;
opacity: 0;
transition: opacity 0.3s;
&::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: $color-light-grey transparent transparent
transparent;
}
}
}
.api__label--text {
cursor: pointer;
}
}
.api__key--holder,
.password--holder,
.customhost__mode--holder {
font-family: $font-nunito;
width: 100%;
border-radius: 100px;
background-color: $color-white;
border: none;
box-shadow: $color-shadow-black 0px 10px 35px;
color: $color-medium-grey;
box-sizing: border-box;
border-color: currentcolor currentcolor $color-border-white;
border-style: none none solid;
border-width: medium medium 4px;
border-image: none 100% / 1 / 0 stretch;
height: 54px;
}
.api__key--holder {
margin-bottom: 3em;
padding: 12px 25px 12px 25px;
font-size: 16px;
}
.password--holder,
.customhost__mode--holder {
font-size: 20px;
padding: 8px 25px 8px 25px;
}
.view__password--eye {
text-transform: uppercase;
cursor: pointer;
position: relative;
float: right;
margin-right: 20px;
margin-top: -36px;
z-index: 2;
}
.saved__alert {
margin-top: 2em;
padding-left: 0.5em;
}
.button__submit {
font-family: $font-nunito;
font-size: 18px;
display: block;
color: $color-white;
width: 100%;
background: $color-null-black
linear-gradient(
to right,
$color-grad-light-blue,
$color-grad-dark-blue
)
repeat scroll 0% 0%;
box-shadow: $color-light-azure 0px 5px 6px;
border: none;
border-radius: 100px;
padding: 12px;
margin: 2em 0 1em 0;
&:hover {
cursor: pointer;
background: $color-null-black
linear-gradient(
to right,
$color-grad-dark-blue,
$color-light-blue
)
repeat scroll 0% 0%;
}
}
}
}
.footer__text--holder {
font-size: 12px;
padding-top: 20px;
padding-bottom: 20px;
letter-spacing: 1px;
.github__repo--link {
border-bottom: 1px dotted $color-black;
text-decoration: none;
}
}
}
}

View File

@@ -1,87 +0,0 @@
@import "base/fonts";
@import "base/reset";
@import "base/variables";
body {
color: $color-black;
}
#home {
min-width: 340px;
.container {
padding: 10px;
.header {
display: flex;
align-items: center;
justify-content: space-between;
line-height: 1;
.main__list--holder {
.list__button {
display: inline-block;
margin-right: 11px;
}
}
}
.content__holder {
.url__content--holder {
display: flex;
align-items: center;
justify-content: center;
margin: 1.5em 0;
.url__content--url {
display: flex;
align-items: center;
#url__content-inner {
margin: 0;
font-size: 20px;
line-height: 1;
color: $color-dark-grey;
font-weight: $bold;
letter-spacing: 0.03em;
}
#copy__alert {
position: absolute;
margin-top: 3.6em;
top: 0px;
color: green;
font-size: 11px;
}
}
.buttons__content--holder {
display: flex;
align-items: center;
padding: 0;
margin: 0 0 0 16px;
.copy__content--holder,
.qrbtn__content--holder {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
margin: 0 6px;
border-radius: 100%;
box-shadow: $color-shadow-light 0px 2px 4px;
background-color: $color-less-white;
#button__copy,
#button__qrcode {
width: 13px;
height: 13px;
}
}
}
}
}
}
}

59
src/util/autoSave.ts Normal file
View File

@@ -0,0 +1,59 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { connect, FormikContextType } from 'formik';
import { useRef, useEffect, useState } from 'react';
import isEqual from 'lodash.isequal';
interface FormikPartProperties {
formik: FormikContextType<any>;
}
interface OuterProperties {
onSave: (values: any) => Promise<any>;
render: any;
}
const usePrevious = (value: any): any => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
/**
* @returns boolean
*
* Wrapping with formik HOC can give access to form values.
* It calls the passed callback with form values as arguments.
*
* Ref: https://github.com/jaredpalmer/formik/issues/172#issuecomment-528192124
*/
const AutoSave = ({ formik: { values }, onSave, render }: OuterProperties & FormikPartProperties): any => {
const previousValues = usePrevious(values);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
function callback(value: any): any {
// promise fulfilled
setIsSaving(false);
return value;
}
function save(): void {
if (previousValues && Object.keys(previousValues).length && !isEqual(previousValues, values)) {
// values are updated
setIsSaving(true);
// invoke passed promise callback
onSave(values).then(callback, callback);
}
}
save();
}, [onSave, previousValues, values]);
return render({ isSaving });
};
export default connect<OuterProperties, any>(AutoSave);

16
src/util/mesageUtil.ts Normal file
View File

@@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { browser } from 'webextension-polyfill-ts';
const messageUtil = {
send(name: string, params?: any): Promise<any> {
const data = {
action: name,
params,
};
return browser.runtime.sendMessage(data);
},
};
export default messageUtil;

33
src/util/settings.ts Normal file
View File

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

12
src/util/tabs.ts Normal file
View File

@@ -0,0 +1,12 @@
import { browser, Tabs } from 'webextension-polyfill-ts';
export function openExtOptionsPage(): Promise<void> {
return browser.runtime.openOptionsPage();
}
export function getCurrentTab(): Promise<Tabs.Tab[]> {
return browser.tabs.query({
active: true,
lastFocusedWindow: true,
});
}

54
tsconfig.json Normal file
View File

@@ -0,0 +1,54 @@
{
"compilerOptions": {
/* Basic Options */
"outDir": "dist",
/* for manifest/index.js */
"allowJs": true,
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"jsx": "react",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"removeComments": true, /* Do not emit comments to output. */
"noEmit": true, /* Do not emit outputs. */
"noEmitOnError": true,
"esModuleInterop": true,
"isolatedModules": true,
/* Strict Type-Checking Options */
"strict": true,
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Additional Checks */
"allowSyntheticDefaultImports": true,
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"useDefineForClassFields": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
/* Module Resolution Options */
"moduleResolution": "node",
"resolveJsonModule": true,
"declaration": true,
"pretty": true,
"newLine": "lf",
"stripInternal": true,
"noUnusedLocals": true,
},
"include": [
"src/**/*", "src/manifest/index.js"
],
"exclude": [
"node_modules"
],
"awesomeTypescriptLoaderOptions": {
"useBabel": true,
"babelCore": "@babel/core", // needed for Babel v7,
"useCache": true, // Use internal file cache to improve warm-up time.
}
}

11
views/options.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=500" />
<title>Options: Kutt</title>
</head>
<body>
<div id="options-root"></div>
</body>
</html>

11
views/popup.html Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=500" />
<title>Kutt Extension</title>
</head>
<body>
<div id="popup-root"></div>
</body>
</html>

View File

@@ -1,20 +1,38 @@
/* eslint-disable global-require, import/no-extraneous-dependencies */
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const WriteWebpackPlugin = require('write-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const ZipPlugin = require('zip-webpack-plugin');
const wextManifest = require('wext-manifest');
const path = require('path');
const webpack = require('webpack');
const wextManifest = require('wext-manifest');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const WriteWebpackPlugin = require('write-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { CheckerPlugin } = require('awesome-typescript-loader');
const ExtensionReloader = require('webpack-extension-reloader');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const manifestInput = require('./src/manifest');
const targetBrowser = process.env.TARGET_BROWSER;
const sourcePath = path.join(__dirname, 'src');
const viewsPath = path.join(__dirname, 'views');
const destPath = path.join(__dirname, 'extension');
const nodeEnv = process.env.NODE_ENV || 'development';
const manifest = wextManifest[targetBrowser](manifestInput);
const extensionReloader =
nodeEnv === 'development'
? new ExtensionReloader({
port: 9128,
reloadPage: true,
entries: {
// TODO: reload manifest on update
background: 'background',
extensionPage: ['popup', 'options'],
},
})
: () => {
this.apply = () => {};
};
const getExtensionFileType = () => {
if (targetBrowser === 'opera') {
return 'crx';
@@ -22,139 +40,98 @@ const getExtensionFileType = () => {
if (targetBrowser === 'firefox') {
return 'xpi';
}
return 'zip';
};
module.exports = () => {
return {
entry: {
options: ['./src/scripts/options.js'],
popup: ['./src/scripts/popup.js'],
history: ['./src/scripts/history.js'],
background: ['./src/scripts/background.js'],
styles: ['./src/styles/popup.scss', './src/styles/history.scss', './src/styles/options.scss'],
module.exports = {
mode: nodeEnv,
entry: {
background: path.join(sourcePath, 'Background', 'index.ts'),
options: path.join(sourcePath, 'Options', 'index.tsx'),
popup: path.join(sourcePath, 'Popup', 'index.tsx'),
},
output: {
filename: 'js/[name].bundle.js',
path: path.join(destPath, targetBrowser),
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
alias: {
'webextension-polyfill-ts': path.resolve(path.join(__dirname, 'node_modules', 'webextension-polyfill-ts')),
},
output: {
path: path.resolve(__dirname, 'extension', targetBrowser),
filename: 'js/[name].js',
publicPath: '',
},
node: {
fs: 'empty',
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader',
},
{
test: /\.(html)$/,
use: {
loader: 'html-loader',
},
module: {
rules: [
{
test: /\.(js|ts|tsx)?$/,
loader: 'awesome-typescript-loader',
exclude: /node_modules/,
},
{
test: /\.(sa|sc|c)ss$/,
use: [
{
loader: MiniCssExtractPlugin.loader, // It creates a CSS file per JS file which contains CSS
},
{
loader: 'css-loader', // Takes the CSS files and returns the CSS with imports and url(...) for Webpack
options: {
attrs: [':data-src'],
sourceMap: true,
},
},
},
{
test: /\.svg$/,
loader: 'url-loader',
},
{
test: /\.scss$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].css',
context: './src/styles/',
outputPath: 'css/',
},
{
loader: 'postcss-loader', // For autoprefixer
options: {
ident: 'postcss',
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
plugins: [require('autoprefixer')()],
},
{
loader: 'extract-loader',
},
{
loader: 'css-loader',
options: {
sourceMap: true,
},
},
{
loader: 'resolve-url-loader',
},
{
loader: 'postcss-loader',
options: {
plugins() {
return [require('precss'), require('autoprefixer')];
},
},
},
{
loader: 'sass-loader',
},
],
},
],
},
plugins: [
new FixStyleOnlyEntriesPlugin({ silent: true }),
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [
path.join(process.cwd(), `extension/${targetBrowser}`),
path.join(process.cwd(), `extension/${targetBrowser}.${getExtensionFileType()}`),
},
'resolve-url-loader', // Rewrites relative paths in url() statements
'sass-loader', // Takes the Sass/SCSS file and compiles to the CSS
],
cleanStaleWebpackAssets: false,
verbose: true,
}),
new CopyWebpackPlugin([{ from: 'src/assets', to: 'assets' }]),
new HtmlWebpackPlugin({
template: 'src/options.html',
inject: false,
filename: 'options.html',
}),
new HtmlWebpackPlugin({
template: 'src/popup.html',
inject: false,
filename: 'popup.html',
}),
new HtmlWebpackPlugin({
template: 'src/history.html',
inject: false,
filename: 'history.html',
}),
new WriteWebpackPlugin([{ name: manifest.name, data: Buffer.from(manifest.content) }]),
},
],
optimization: {
minimizer: [
new OptimizeCssAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorOptions: {
map: false,
},
cssProcessorPluginOptions: {
preset: ['default', { discardComments: { removeAll: true } }],
},
canPrint: true,
}),
new TerserPlugin({
cache: true,
parallel: true,
}),
new ZipPlugin({
path: path.resolve(__dirname, 'extension'),
extension: `${getExtensionFileType()}`,
filename: `${targetBrowser}`,
}),
},
plugins: [
// for awesome-typescript-loader
new CheckerPlugin(),
// environment variables
new webpack.EnvironmentPlugin(['NODE_ENV', 'TARGET_BROWSER']),
// delete previous build files
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [
path.join(process.cwd(), `extension/${targetBrowser}`),
path.join(process.cwd(), `extension/${targetBrowser}.${getExtensionFileType()}`),
],
},
devServer: {
port: 3000,
contentBase: './extension',
},
};
cleanStaleWebpackAssets: false,
verbose: true,
}),
new HtmlWebpackPlugin({
template: path.join(viewsPath, 'popup.html'),
inject: 'body',
filename: 'popup.html',
chunks: ['popup'],
}),
new HtmlWebpackPlugin({
template: path.join(viewsPath, 'options.html'),
inject: 'body',
filename: 'options.html',
chunks: ['options'],
}),
// write css file(s) to build folder
new MiniCssExtractPlugin({ filename: 'css/[name].css' }),
// copy static assets
new CopyWebpackPlugin([{ from: path.join(sourcePath, 'assets'), to: 'assets' }]),
// write manifest.json
new WriteWebpackPlugin([{ name: manifest.name, data: Buffer.from(manifest.content) }]),
// plugin to enable browser reloading in development mode
extensionReloader,
],
};

4777
yarn.lock

File diff suppressed because it is too large Load Diff