feat: migrate to vite and manifest v3
42
.babelrc
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
// Latest stable ECMAScript features
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": false,
|
||||
// Do not transform modules to CJS
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"chrome": "49",
|
||||
"firefox": "52",
|
||||
"opera": "36",
|
||||
"edge": "79"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/typescript",
|
||||
"@babel/react"
|
||||
],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-class-properties"],
|
||||
["@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
|
||||
}
|
||||
],
|
||||
// Support for twin.macro
|
||||
"babel-plugin-macros",
|
||||
// https://git.io/JJUrL
|
||||
"@babel/plugin-transform-react-jsx"
|
||||
]
|
||||
}
|
||||
76
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
- name: Build for all browsers
|
||||
run: npm run build
|
||||
|
||||
- name: Upload Chrome extension artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: chrome-extension
|
||||
path: extension/chrome.zip
|
||||
|
||||
- name: Upload Firefox extension artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: firefox-extension
|
||||
path: extension/firefox.xpi
|
||||
|
||||
- name: Upload Opera extension artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: opera-extension
|
||||
path: extension/opera.crx
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
- name: Build for all browsers
|
||||
run: npm run build
|
||||
|
||||
- name: Deploy to extension branch
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./extension
|
||||
publish_branch: extension
|
||||
keep_files: false
|
||||
19
.travis.yml
@@ -1,19 +0,0 @@
|
||||
language: node_js
|
||||
cache:
|
||||
directories:
|
||||
- ~/.npm
|
||||
node_js:
|
||||
- 12
|
||||
git:
|
||||
depth: 3
|
||||
script:
|
||||
- yarn run build
|
||||
deploy:
|
||||
provider: pages
|
||||
skip-cleanup: true
|
||||
keep-history: true
|
||||
github-token: $GITHUB_TOKEN
|
||||
local-dir: extension
|
||||
target-branch: extension
|
||||
on:
|
||||
branch: master
|
||||
@@ -1,9 +1,9 @@
|
||||
<div align="center"><img width="150" src="source/assets/logo.png" /></div>
|
||||
<div align="center"><img width="150" src="source/public/assets/logo.png" /></div>
|
||||
<h1 align="center">kutt-extension</h1>
|
||||
<p align="center">Browser extension for <a href="https://kutt.it">Kutt.it</a></p>
|
||||
<div align="center">
|
||||
<a href="https://travis-ci.com/thedevs-network/kutt-extension">
|
||||
<img src="https://travis-ci.com/thedevs-network/kutt-extension.svg?branch=master" alt="Travis Build" />
|
||||
<a href="https://github.com/thedevs-network/kutt-extension/actions/workflows/build.yml">
|
||||
<img src="https://github.com/thedevs-network/kutt-extension/actions/workflows/build.yml/badge.svg?branch=master" alt="Build" />
|
||||
</a>
|
||||
<a href="https://github.com/thedevs-network/kutt-extension/releases/latest">
|
||||
<img src="https://img.shields.io/github/release/thedevs-network/kutt-extension.svg?colorB=blue" alt="Releases" />
|
||||
@@ -14,9 +14,6 @@
|
||||
<a href="https://github.com/thedevs-network/kutt-extension/issues?q=is%3Aissue+is%3Aclosed">
|
||||
<img src="https://img.shields.io/github/issues-closed-raw/thedevs-network/kutt-extension.svg?colorB=red" alt="Closed Issues" />
|
||||
</a>
|
||||
<a href="https://david-dm.org/thedevs-network/kutt-extension">
|
||||
<img src="https://img.shields.io/david/thedevs-network/kutt-extension.svg?colorB=orange" alt="DEPENDENCIES" />
|
||||
</a>
|
||||
<a href="https://github.com/thedevs-network/kutt-extension/blob/master/license">
|
||||
<img src="https://img.shields.io/github/license/thedevs-network/kutt-extension.svg" alt="LICENSE" />
|
||||
</a>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
twin: {
|
||||
preset: 'styled-components',
|
||||
config: './tailwind.config.js',
|
||||
autoCssProp: true, // This adds the css prop when it's needed
|
||||
},
|
||||
};
|
||||
19
cssprop.d.ts
vendored
@@ -1,19 +0,0 @@
|
||||
import {} from 'react';
|
||||
import {CSSProp} from 'styled-components';
|
||||
|
||||
declare module 'react' {
|
||||
interface Attributes {
|
||||
// NOTE: unlike the plain javascript version, it is not possible to get access
|
||||
// to the element's own attributes inside function interpolations.
|
||||
// Only theme will be accessible, and only with the DefaultTheme due to the global
|
||||
// nature of this declaration.
|
||||
// If you are writing this inline you already have access to all the attributes anyway,
|
||||
// no need for the extra indirection.
|
||||
/**
|
||||
* If present, this React element will be converted by
|
||||
* `babel-plugin-styled-components` into a styled component
|
||||
* with the given css as its styles.
|
||||
*/
|
||||
css?: CSSProp;
|
||||
}
|
||||
}
|
||||
7786
package-lock.json
generated
Normal file
138
package.json
@@ -10,29 +10,19 @@
|
||||
"url": "https://abhijithvijayan.in"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0",
|
||||
"yarn": ">=1.0.0"
|
||||
"node": ">=20"
|
||||
},
|
||||
"type": "module",
|
||||
"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,.tsx",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
"eslint . --ext .ts,.tsx"
|
||||
]
|
||||
"dev:chrome": "cross-env TARGET_BROWSER=chrome vite build --config vite.config.ts --mode development --watch",
|
||||
"dev:firefox": "cross-env TARGET_BROWSER=firefox vite build --config vite.config.ts --mode development --watch",
|
||||
"dev:opera": "cross-env TARGET_BROWSER=opera vite build --config vite.config.ts --mode development --watch",
|
||||
"build:chrome": "cross-env TARGET_BROWSER=chrome vite build --config vite.config.ts",
|
||||
"build:firefox": "cross-env TARGET_BROWSER=firefox vite build --config vite.config.ts",
|
||||
"build:opera": "cross-env TARGET_BROWSER=opera vite build --config vite.config.ts",
|
||||
"build": "npm run build:chrome && npm run build:firefox && npm run build:opera",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"keywords": [
|
||||
"url",
|
||||
@@ -45,78 +35,44 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@abhijithvijayan/ts-utils": "^1.2.2",
|
||||
"@babel/runtime": "^7.14.6",
|
||||
"advanced-css-reset": "^1.2.2",
|
||||
"axios": "^0.21.1",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-use-form-state": "^0.13.2",
|
||||
"styled-components": "^5.3.0",
|
||||
"twin.macro": "^1.12.1",
|
||||
"webextension-polyfill-ts": "^0.26.0"
|
||||
"advanced-css-reset": "^2.1.3",
|
||||
"axios": "^1.7.9",
|
||||
"clsx": "^2.1.1",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"webextension-polyfill": "^0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@abhijithvijayan/eslint-config": "2.6.3",
|
||||
"@abhijithvijayan/eslint-config-airbnb": "^1.0.2",
|
||||
"@abhijithvijayan/tsconfig": "^1.3.0",
|
||||
"@babel/core": "^7.14.6",
|
||||
"@babel/eslint-parser": "^7.14.7",
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.14.7",
|
||||
"@babel/plugin-transform-destructuring": "^7.14.7",
|
||||
"@babel/plugin-transform-react-jsx": "^7.14.5",
|
||||
"@babel/plugin-transform-runtime": "^7.14.5",
|
||||
"@babel/preset-env": "^7.14.7",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@babel/preset-typescript": "^7.14.5",
|
||||
"@types/lodash.isequal": "^4.5.5",
|
||||
"@types/node": "^14.17.5",
|
||||
"@types/qrcode.react": "^1.0.2",
|
||||
"@types/react": "^17.0.14",
|
||||
"@types/react-copy-to-clipboard": "^5.0.1",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/styled-components": "^5.1.11",
|
||||
"@types/webpack": "^4.41.30",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.3",
|
||||
"@typescript-eslint/parser": "^4.28.3",
|
||||
"autoprefixer": "^10.3.1",
|
||||
"babel-loader": "^8.2.2",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"@abhijithvijayan/eslint-config": "^3.0.0",
|
||||
"@abhijithvijayan/tsconfig": "^1.5.1",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-copy-to-clipboard": "^5.0.7",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/webextension-polyfill": "^0.12.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.1",
|
||||
"@typescript-eslint/parser": "^8.18.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5.2.7",
|
||||
"eslint": "^7.30.0",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"filemanager-webpack-plugin": "^3.1.1",
|
||||
"fork-ts-checker-webpack-plugin": "^6.2.12",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"husky": "^6.0.0",
|
||||
"lint-staged": "^11.0.1",
|
||||
"mini-css-extract-plugin": "^1.6.2",
|
||||
"node-sass": "^4.14.1",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.8",
|
||||
"postcss": "^8.3.5",
|
||||
"postcss-loader": "^4.3.0",
|
||||
"prettier": "^2.3.2",
|
||||
"resolve-url-loader": "^3.1.4",
|
||||
"sass-extract": "^2.1.0",
|
||||
"sass-extract-js": "^0.4.0",
|
||||
"sass-extract-loader": "^1.1.0",
|
||||
"sass-loader": "^10.2.0",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"typescript": "4.3.5",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^4.7.2",
|
||||
"webpack-extension-reloader": "^1.1.4",
|
||||
"wext-manifest-loader": "^2.3.0",
|
||||
"wext-manifest-webpack-plugin": "^1.2.1"
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import-x": "^4.5.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-n": "^17.15.1",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"globals": "^15.14.0",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.4.2",
|
||||
"sass": "^1.83.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.5",
|
||||
"vite-plugin-checker": "^0.8.0",
|
||||
"vite-plugin-wext-manifest": "^1.2.2",
|
||||
"vite-plugin-zip-pack": "^1.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
5
postcss.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import autoprefixer from 'autoprefixer';
|
||||
|
||||
export default {
|
||||
plugins: [autoprefixer()],
|
||||
};
|
||||
@@ -6,9 +6,8 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {browser} from 'webextension-polyfill-ts';
|
||||
|
||||
import axios, {AxiosPromise} from 'axios';
|
||||
import browser, {Runtime} from 'webextension-polyfill';
|
||||
import axios, {AxiosPromise, AxiosError} from 'axios';
|
||||
import * as constants from './constants';
|
||||
|
||||
export enum Kutt {
|
||||
@@ -113,7 +112,8 @@ async function shortenUrl({
|
||||
error: false,
|
||||
data,
|
||||
};
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
const err = error as AxiosError<{error?: string}>;
|
||||
if (err.response) {
|
||||
if (err.response.status === 401) {
|
||||
return {
|
||||
@@ -129,7 +129,7 @@ async function shortenUrl({
|
||||
) {
|
||||
return {
|
||||
error: true,
|
||||
message: `Error: ${err.response.data.error}`,
|
||||
message: `Error: ${err.response.data?.error}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -201,7 +201,8 @@ async function checkApiKey({
|
||||
error: false,
|
||||
data,
|
||||
};
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
const err = error as AxiosError;
|
||||
if (err.response) {
|
||||
if (err.response.status === 401) {
|
||||
return {
|
||||
@@ -283,7 +284,8 @@ async function fetchUrlsHistory({
|
||||
error: false,
|
||||
data,
|
||||
};
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
const err = error as AxiosError;
|
||||
if (err.response) {
|
||||
if (err.response.status === 401) {
|
||||
return {
|
||||
@@ -314,11 +316,24 @@ async function fetchUrlsHistory({
|
||||
|
||||
// **** ------------------ **** //
|
||||
|
||||
/**
|
||||
* Service worker installation listener (MV3)
|
||||
*/
|
||||
browser.runtime.onInstalled.addListener((): void => {
|
||||
console.log('Kutt extension installed');
|
||||
});
|
||||
|
||||
type MessageRequest = {
|
||||
action: string;
|
||||
params: any;
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for messages from UI pages
|
||||
*/
|
||||
browser.runtime.onMessage.addListener(
|
||||
(request, _sender): void | Promise<any> => {
|
||||
(message: unknown, _sender: Runtime.MessageSender): void | Promise<any> => {
|
||||
const request = message as MessageRequest;
|
||||
// eslint-disable-next-line consistent-return
|
||||
// eslint-disable-next-line default-case
|
||||
switch (request.action) {
|
||||
|
||||
21
source/History/History.module.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.historyPage {
|
||||
min-height: 100vh;
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
.historyContent {
|
||||
overflow-x: hidden;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font-size: 1.25rem;
|
||||
font-weight: $medium;
|
||||
color: $gray-800;
|
||||
}
|
||||
|
||||
.loaderContainer {
|
||||
height: 2.5rem;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import 'twin.macro';
|
||||
import {useEffect, useState} from 'react';
|
||||
|
||||
import {
|
||||
useShortenedLinks,
|
||||
@@ -30,7 +29,9 @@ import Loader from '../components/Loader';
|
||||
import Header from '../Options/Header';
|
||||
import Table from './Table';
|
||||
|
||||
const History: React.FC = () => {
|
||||
import styles from './History.module.scss';
|
||||
|
||||
function History() {
|
||||
const [, shortenedLinksDispatch] = useShortenedLinks();
|
||||
const [, extensionSettingsDispatch] = useExtensionSettings();
|
||||
const [requestStatusState, requestStatusDispatch] = useRequestStatus();
|
||||
@@ -56,7 +57,7 @@ const History: React.FC = () => {
|
||||
.replace('http://', '')
|
||||
.replace('https://', '')
|
||||
.replace('www.', '')
|
||||
.split(/[/?#]/)[0], // extract domain
|
||||
.split(/[/?#]/)[0] || '', // extract domain
|
||||
hostUrl: (settings.host as string).endsWith('/')
|
||||
? (settings.host as string).slice(0, -1)
|
||||
: (settings.host as string), // slice `/` at the end
|
||||
@@ -129,8 +130,8 @@ const History: React.FC = () => {
|
||||
|
||||
return (
|
||||
<BodyWrapper>
|
||||
<div id="history" tw="h-screen bg-gray-200">
|
||||
<div tw={'overflow-x-hidden px-6 py-8'}>
|
||||
<div id="history" className={styles.historyPage}>
|
||||
<div className={styles.historyContent}>
|
||||
<Header />
|
||||
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
@@ -138,10 +139,10 @@ const History: React.FC = () => {
|
||||
!errored.error ? (
|
||||
<Table />
|
||||
) : (
|
||||
<h2>{errored.message}</h2>
|
||||
<h2 className={styles.errorMessage}>{errored.message}</h2>
|
||||
)
|
||||
) : (
|
||||
<div tw="h-10">
|
||||
<div className={styles.loaderContainer}>
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
@@ -149,6 +150,6 @@ const History: React.FC = () => {
|
||||
</div>
|
||||
</BodyWrapper>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default History;
|
||||
|
||||
57
source/History/Modal.module.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.modalOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(50, 50, 50, 0.8);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modalContent {
|
||||
padding: 3rem 4rem;
|
||||
text-align: center;
|
||||
background-color: $white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.qrCodeWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 2.5rem;
|
||||
padding: 0 2rem;
|
||||
margin: 0 1rem;
|
||||
overflow: hidden;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
border-radius: 100px;
|
||||
border: none;
|
||||
transition: all 0.4s ease-out;
|
||||
background: linear-gradient(to right, rgb(224, 224, 224), rgb(189, 189, 189));
|
||||
box-shadow: rgba(160, 160, 160, 0.5) 0px 5px 6px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,26 @@
|
||||
import tw, {css} from 'twin.macro';
|
||||
import QRCode from 'qrcode.react';
|
||||
import React from 'react';
|
||||
import {QRCodeSVG} from 'qrcode.react';
|
||||
import {Dispatch, SetStateAction} from 'react';
|
||||
|
||||
import styles from './Modal.module.scss';
|
||||
|
||||
type Props = {
|
||||
link: string;
|
||||
setModalView: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setModalView: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const Modal: React.FC<Props> = ({link, setModalView}) => {
|
||||
function Modal({link, setModalView}: Props) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
css={[
|
||||
tw`fixed top-0 left-0 flex items-center justify-center w-full h-full`,
|
||||
|
||||
css`
|
||||
background-color: rgba(50, 50, 50, 0.8);
|
||||
z-index: 1000;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<div
|
||||
css={[
|
||||
tw`px-16 py-12 text-center bg-white`,
|
||||
|
||||
css`
|
||||
border-radius: 8px;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
<QRCode size={196} value={link} />
|
||||
<div className={styles.modalOverlay}>
|
||||
<div className={styles.modalContent}>
|
||||
<div className={styles.qrCodeWrapper}>
|
||||
<QRCodeSVG size={196} value={link} />
|
||||
</div>
|
||||
|
||||
<div tw="flex justify-center mt-10">
|
||||
<div className={styles.buttonWrapper}>
|
||||
<button
|
||||
onClick={(): void => setModalView(false)}
|
||||
css={[
|
||||
tw`relative flex items-center justify-center h-10 px-8 py-0 mx-4 my-0 overflow-hidden text-sm leading-none text-center text-black transition-all ease-out cursor-pointer`,
|
||||
|
||||
css`
|
||||
border-radius: 100px;
|
||||
transition-duration: 400ms;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgb(224, 224, 224),
|
||||
rgb(189, 189, 189)
|
||||
);
|
||||
box-shadow: rgba(160, 160, 160, 0.5) 0px 5px 6px;
|
||||
`,
|
||||
]}
|
||||
className={styles.closeButton}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
@@ -59,6 +30,6 @@ const Modal: React.FC<Props> = ({link, setModalView}) => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Modal;
|
||||
|
||||
199
source/History/Table.module.scss
Normal file
@@ -0,0 +1,199 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.tableContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
margin-top: 1.25rem;
|
||||
margin-bottom: 0.75rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tableWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
width: 1200px;
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tableTitle {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
background-color: $white;
|
||||
border-radius: 12px;
|
||||
box-shadow: rgba(50, 50, 50, 0.2) 0px 6px 30px;
|
||||
}
|
||||
|
||||
.thead {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
background-color: rgb(241, 241, 241);
|
||||
border-top-right-radius: 12px;
|
||||
border-top-left-radius: 12px;
|
||||
}
|
||||
|
||||
.theadRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: auto;
|
||||
padding: 0 1.5rem;
|
||||
border-bottom: 1px solid rgb(234, 234, 234);
|
||||
}
|
||||
|
||||
.th {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 1rem 0;
|
||||
font-size: 1rem;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.thOriginal {
|
||||
flex: 2 2 0px;
|
||||
}
|
||||
|
||||
.thShort {
|
||||
flex: 1 1 0px;
|
||||
}
|
||||
|
||||
.tbody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.tr {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: auto;
|
||||
padding: 0 1.25rem;
|
||||
border-bottom: 1px solid rgb(234, 234, 234);
|
||||
}
|
||||
|
||||
.td {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.tdOriginal {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex: 2 2 0px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
width: 56px;
|
||||
background: linear-gradient(to left, white, white, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.tdShort {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
flex: 1 1 23px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
width: 56px;
|
||||
background: linear-gradient(to left, white, white, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
font-size: 1rem;
|
||||
line-height: normal;
|
||||
text-decoration: none;
|
||||
color: rgb(33, 150, 243);
|
||||
border: 1px solid transparent;
|
||||
border-style: dotted;
|
||||
transition: all 0.2s ease-out;
|
||||
|
||||
&:hover {
|
||||
border-color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.copiedNotification {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
font-size: 11px;
|
||||
color: $green-900;
|
||||
}
|
||||
|
||||
.shortUrlWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actionsWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-right: 2px;
|
||||
margin-left: 12px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
box-shadow: rgba(100, 100, 100, 0.1) 0px 2px 4px;
|
||||
background-color: rgb(222, 222, 222);
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
svg {
|
||||
stroke: rgb(101, 189, 137);
|
||||
stroke-width: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.emptyRow {
|
||||
padding: 1rem;
|
||||
color: $gray-600;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import tw, {css, styled} from 'twin.macro';
|
||||
import {useEffect, useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import {
|
||||
useShortenedLinks,
|
||||
@@ -11,39 +11,16 @@ import {MAX_HISTORY_ITEMS} from '../Background/constants';
|
||||
import Icon from '../components/Icon';
|
||||
import Modal from './Modal';
|
||||
|
||||
const StyledTd = styled.td`
|
||||
${tw`relative flex items-center px-0 py-4`}
|
||||
`;
|
||||
import styles from './Table.module.scss';
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
${tw`flex items-center justify-center p-0 my-0 transition-all duration-200 ease-out border-none outline-none cursor-pointer`}
|
||||
|
||||
margin-right: 2px;
|
||||
margin-left: 12px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
box-shadow: rgba(100, 100, 100, 0.1) 0px 2px 4px;
|
||||
background-color: rgb(222, 222, 222);
|
||||
border-radius: 100%;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
svg {
|
||||
stroke: rgb(101, 189, 137);
|
||||
stroke-width: 2;
|
||||
}
|
||||
`;
|
||||
|
||||
const Table: React.FC = () => {
|
||||
function Table() {
|
||||
const [shortenedLinksState, shortenedLinksDispatch] = useShortenedLinks();
|
||||
const [QRView, setQRView] = useState<boolean>(false);
|
||||
const [copied, setCopied] = useState<boolean>(false);
|
||||
|
||||
// reset copy message
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
timer = setTimeout(() => {
|
||||
setCopied(false);
|
||||
@@ -77,186 +54,49 @@ const Table: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
css={[
|
||||
tw` flex flex-col items-center w-full min-h-screen mt-5 mb-3`,
|
||||
|
||||
css`
|
||||
flex: 0 0 auto;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<div
|
||||
css={[
|
||||
tw`flex flex-col mx-0`,
|
||||
|
||||
css`
|
||||
width: 1200px;
|
||||
max-width: 95%;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<div tw="flex items-center justify-center">
|
||||
<h2 css={[tw`mx-0 text-xl mb-5`]}>
|
||||
<div className={styles.tableContainer}>
|
||||
<div className={styles.tableWrapper}>
|
||||
<div className={styles.tableHeader}>
|
||||
<h2 className={styles.tableTitle}>
|
||||
Recent shortened links. (last {MAX_HISTORY_ITEMS} results)
|
||||
</h2>
|
||||
</div>
|
||||
<table
|
||||
css={[
|
||||
tw`flex flex-col flex-auto bg-white`,
|
||||
|
||||
css`
|
||||
border-radius: 12px;
|
||||
box-shadow: rgba(50, 50, 50, 0.2) 0px 6px 30px;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<thead
|
||||
css={[
|
||||
tw`flex flex-col flex-auto`,
|
||||
|
||||
css`
|
||||
background-color: rgb(241, 241, 241);
|
||||
border-top-right-radius: 12px;
|
||||
border-top-left-radius: 12px;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<tr
|
||||
css={[
|
||||
tw`flex justify-between flex-auto px-6 py-0`,
|
||||
|
||||
css`
|
||||
border-bottom: 1px solid rgb(234, 234, 234);
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<th
|
||||
css={[
|
||||
tw`relative flex items-center justify-start px-0 py-4 text-base leading-normal`,
|
||||
|
||||
css`
|
||||
flex: 2 2 0px;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<table className={styles.table}>
|
||||
<thead className={styles.thead}>
|
||||
<tr className={styles.theadRow}>
|
||||
<th className={clsx(styles.th, styles.thOriginal)}>
|
||||
Original URL
|
||||
</th>
|
||||
<th
|
||||
css={[
|
||||
tw`relative flex items-center justify-start px-0 py-4 text-base leading-normal`,
|
||||
|
||||
css`
|
||||
flex: 1 1 0px;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
Short URL
|
||||
</th>
|
||||
<th className={clsx(styles.th, styles.thShort)}>Short URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody tw="flex flex-col flex-auto">
|
||||
<tbody className={styles.tbody}>
|
||||
{!(shortenedLinksState.total === 0) ? (
|
||||
shortenedLinksState.items.map((item) => {
|
||||
return (
|
||||
<tr
|
||||
key={item.id}
|
||||
css={[
|
||||
tw`flex justify-between flex-auto px-5 py-0`,
|
||||
|
||||
css`
|
||||
border-bottom: 1px solid rgb(234, 234, 234);
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<StyledTd
|
||||
css={[
|
||||
tw`relative overflow-hidden whitespace-no-wrap`,
|
||||
|
||||
css`
|
||||
flex: 2 2 0px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
width: 56px;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
white,
|
||||
white,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<tr key={item.id} className={styles.tr}>
|
||||
<td className={clsx(styles.td, styles.tdOriginal)}>
|
||||
<a
|
||||
css={[
|
||||
tw`hover:border-black text-base leading-normal no-underline transition-all duration-200 ease-out border border-transparent border-dotted`,
|
||||
|
||||
css`
|
||||
color: rgb(33, 150, 243);
|
||||
`,
|
||||
]}
|
||||
className={styles.link}
|
||||
href={item.target}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
{item.target}
|
||||
</a>
|
||||
</StyledTd>
|
||||
</td>
|
||||
|
||||
<StyledTd
|
||||
css={[
|
||||
tw`relative overflow-hidden whitespace-no-wrap`,
|
||||
|
||||
css`
|
||||
flex: 1 1 23px;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
width: 56px;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
white,
|
||||
white,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<td className={clsx(styles.td, styles.tdShort)}>
|
||||
{copied &&
|
||||
shortenedLinksState.selected?.id === item.id && (
|
||||
<div
|
||||
css={[
|
||||
tw`absolute top-0 left-0 text-xs text-green-900`,
|
||||
|
||||
css`
|
||||
font-size: 11px;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<div className={styles.copiedNotification}>
|
||||
Copied to clipboard!
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div tw="flex items-center">
|
||||
<div className={styles.shortUrlWrapper}>
|
||||
<a
|
||||
css={[
|
||||
tw`hover:border-black text-base leading-normal no-underline transition-all duration-200 ease-out border border-transparent border-dotted`,
|
||||
|
||||
css`
|
||||
color: rgb(33, 150, 243);
|
||||
`,
|
||||
]}
|
||||
className={styles.link}
|
||||
href={item.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
@@ -264,15 +104,15 @@ const Table: React.FC = () => {
|
||||
{item.link}
|
||||
</a>
|
||||
</div>
|
||||
</StyledTd>
|
||||
</td>
|
||||
|
||||
<StyledTd>
|
||||
<div tw="flex items-center justify-end">
|
||||
<td className={styles.td}>
|
||||
<div className={styles.actionsWrapper}>
|
||||
{/* // **** COPY TO CLIPBOARD **** // */}
|
||||
|
||||
{copied &&
|
||||
shortenedLinksState.selected?.id === item.id ? (
|
||||
<StyledIcon name="tick" className="icon" />
|
||||
<Icon name="tick" className={styles.actionIcon} />
|
||||
) : (
|
||||
<CopyToClipboard
|
||||
text={item.link}
|
||||
@@ -280,15 +120,15 @@ const Table: React.FC = () => {
|
||||
return handleCopyToClipboard(item.id);
|
||||
}}
|
||||
>
|
||||
<StyledIcon name="copy" className="icon" />
|
||||
<Icon name="copy" className={styles.actionIcon} />
|
||||
</CopyToClipboard>
|
||||
)}
|
||||
|
||||
<StyledIcon
|
||||
<Icon
|
||||
onClick={(): void =>
|
||||
handleQRCodeViewToggle(item.id)
|
||||
}
|
||||
className="icon"
|
||||
className={styles.actionIcon}
|
||||
name="qrcode"
|
||||
/>
|
||||
</div>
|
||||
@@ -298,13 +138,13 @@ const Table: React.FC = () => {
|
||||
shortenedLinksState.selected?.id === item.id && (
|
||||
<Modal link={item.link} setModalView={setQRView} />
|
||||
)}
|
||||
</StyledTd>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td>No URLs History</td>
|
||||
<td className={styles.emptyRow}>No URLs History</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
@@ -313,6 +153,6 @@ const Table: React.FC = () => {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Table;
|
||||
|
||||
12
source/History/history.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>History: Kutt</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="history-root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,21 +1,21 @@
|
||||
import {ThemeProvider} from 'styled-components';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
|
||||
import {ExtensionSettingsProvider} from '../contexts/extension-settings-context';
|
||||
import {RequestStatusProvider} from '../contexts/request-status-context';
|
||||
import {ShortenedLinksProvider} from '../contexts/shortened-links-context';
|
||||
import History from './History';
|
||||
|
||||
// Common styles
|
||||
import './styles.scss';
|
||||
|
||||
import History from './History';
|
||||
import {ExtensionSettingsProvider} from '../contexts/extension-settings-context';
|
||||
import {ShortenedLinksProvider} from '../contexts/shortened-links-context';
|
||||
import {RequestStatusProvider} from '../contexts/request-status-context';
|
||||
const container = document.getElementById('history-root');
|
||||
if (!container) {
|
||||
throw new Error('Could not find history-root container');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax, import/no-unresolved, @typescript-eslint/no-var-requires, node/no-missing-require
|
||||
const theme = require('sass-extract-loader?{"plugins": ["sass-extract-js"]}!../styles/base/_variables.scss');
|
||||
// Require sass variables using sass-extract-loader and specify the plugin
|
||||
|
||||
ReactDOM.render(
|
||||
<ThemeProvider theme={theme}>
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ExtensionSettingsProvider>
|
||||
<RequestStatusProvider>
|
||||
<ShortenedLinksProvider>
|
||||
@@ -23,6 +23,5 @@ ReactDOM.render(
|
||||
</ShortenedLinksProvider>
|
||||
</RequestStatusProvider>
|
||||
</ExtensionSettingsProvider>
|
||||
</ThemeProvider>,
|
||||
document.getElementById('history-root')
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '../styles/main.scss';
|
||||
@use '../styles/main.scss';
|
||||
|
||||
body {
|
||||
background-color: #edf2f7;
|
||||
|
||||
107
source/Options/Footer.module.scss
Normal file
@@ -0,0 +1,107 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.footer {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem 0;
|
||||
font-weight: $regular;
|
||||
font-size: 0.75rem;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ratingSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $gray-800;
|
||||
}
|
||||
|
||||
.dividerLine {
|
||||
display: block;
|
||||
width: 33.333%;
|
||||
border: 1px solid $gray-200;
|
||||
|
||||
&.left {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
&.right {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ratingLink {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.starsContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.starIcon {
|
||||
margin-right: 0.25rem;
|
||||
fill: currentColor;
|
||||
|
||||
&.yellow {
|
||||
color: $yellow-500;
|
||||
}
|
||||
|
||||
&.gray {
|
||||
color: $gray-400;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ratingText {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.linksSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.linkItem {
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $gray-800;
|
||||
}
|
||||
|
||||
&.narrow {
|
||||
width: 33.333%;
|
||||
}
|
||||
|
||||
&.wide {
|
||||
width: 66.666%;
|
||||
}
|
||||
}
|
||||
|
||||
.linkDivider {
|
||||
width: 1px;
|
||||
height: 1rem;
|
||||
background-color: $gray-300;
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
import React from 'react';
|
||||
import 'twin.macro';
|
||||
import {memo} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import {detectBrowser} from '../util/browser';
|
||||
import {StoreLinks} from '../Background';
|
||||
|
||||
import Icon from '../components/Icon';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
import styles from './Footer.module.scss';
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<>
|
||||
<footer tw="sm:mt-6 xl:mt-16 lg:mt-10 md:mt-8 py-4 mt-4 font-normal text-xs">
|
||||
<div tw="flex items-center text-gray-800">
|
||||
<span tw="block w-1/3 mr-2 border border-gray-200" />
|
||||
<footer className={styles.footer}>
|
||||
<div className={styles.ratingSection}>
|
||||
<span className={clsx(styles.dividerLine, styles.left)} />
|
||||
<a
|
||||
href={
|
||||
detectBrowser() === 'firefox'
|
||||
@@ -20,42 +22,44 @@ const Footer: React.FC = () => {
|
||||
}
|
||||
target="_blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
tw="flex flex-col items-center justify-center"
|
||||
className={styles.ratingLink}
|
||||
>
|
||||
<div tw="flex items-center mt-1">
|
||||
<Icon tw="mr-1 text-yellow-500 fill-current" name="star-yellow" />
|
||||
<Icon tw="mr-1 text-yellow-500 fill-current" name="star-yellow" />
|
||||
<Icon tw="mr-1 text-yellow-500 fill-current" name="star-yellow" />
|
||||
<Icon tw="mr-1 text-yellow-500 fill-current" name="star-yellow" />
|
||||
<Icon tw="text-gray-400 fill-current" name="star-white" />
|
||||
<div className={styles.starsContainer}>
|
||||
<Icon className={clsx(styles.starIcon, styles.yellow)} name="star-yellow" />
|
||||
<Icon className={clsx(styles.starIcon, styles.yellow)} name="star-yellow" />
|
||||
<Icon className={clsx(styles.starIcon, styles.yellow)} name="star-yellow" />
|
||||
<Icon className={clsx(styles.starIcon, styles.yellow)} name="star-yellow" />
|
||||
<Icon className={clsx(styles.starIcon, styles.gray)} name="star-white" />
|
||||
</div>
|
||||
<p tw="mb-0 mt-1">Rate on Store</p>
|
||||
<p className={styles.ratingText}>Rate on Store</p>
|
||||
</a>
|
||||
<span tw="block w-1/3 ml-2 border border-gray-200" />
|
||||
<span className={clsx(styles.dividerLine, styles.right)} />
|
||||
</div>
|
||||
|
||||
<div tw="flex items-center justify-around text-center divide-x divide-gray-300 my-4">
|
||||
<div className={styles.linksSection}>
|
||||
<a
|
||||
href="https://kutt.it"
|
||||
target="blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
tw="w-1/3 p-1 cursor-pointer hover:text-gray-800"
|
||||
className={clsx(styles.linkItem, styles.narrow)}
|
||||
>
|
||||
Kutt.it
|
||||
</a>
|
||||
<span className={styles.linkDivider} />
|
||||
<a
|
||||
href={'https://git.io/Jn5hS'}
|
||||
target="blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
tw="w-2/3 p-1 cursor-pointer hover:text-gray-800"
|
||||
className={clsx(styles.linkItem, styles.wide)}
|
||||
>
|
||||
Report an issue
|
||||
</a>
|
||||
<span className={styles.linkDivider} />
|
||||
<a
|
||||
href="https://github.com/thedevs-network/kutt-extension"
|
||||
target="blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
tw="w-1/3 p-1 cursor-pointer hover:text-gray-800"
|
||||
className={clsx(styles.linkItem, styles.narrow)}
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
@@ -63,6 +67,6 @@ const Footer: React.FC = () => {
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default React.memo(Footer);
|
||||
export default memo(Footer);
|
||||
|
||||
211
source/Options/Form.module.scss
Normal file
@@ -0,0 +1,211 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.formSection {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.inputGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
.labelLink {
|
||||
margin-left: 0.5rem;
|
||||
color: $blue-500;
|
||||
text-decoration: none;
|
||||
text-transform: lowercase;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inputIconWrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
width: 2.5rem;
|
||||
height: 100%;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.inputIcon {
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $gray-600;
|
||||
border-radius: $radius-sm 0 0 $radius-sm;
|
||||
}
|
||||
|
||||
.input {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 3rem;
|
||||
font-size: 0.875rem;
|
||||
background-color: $gray-200;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $radius-sm;
|
||||
|
||||
&::placeholder {
|
||||
color: $gray-400;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $indigo-400;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.inputError {
|
||||
border-color: $red-500;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0.25rem;
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: $medium;
|
||||
letter-spacing: 0.025em;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
.validateButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: $semibold;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
background: $primary-gradient;
|
||||
border: none;
|
||||
border-radius: $radius-sm;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: color $transition-normal;
|
||||
|
||||
&:hover {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.validateText {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.validateIcon {
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
|
||||
svg {
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
transition: transform $transition-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.toggleSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.toggleLabel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggleText {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.toggleWrapper {
|
||||
position: relative;
|
||||
width: 2.5rem;
|
||||
height: 1.5rem;
|
||||
margin-left: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggleTrack {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $gray-400;
|
||||
border-radius: 9999px;
|
||||
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
|
||||
transition: background-color $transition-normal;
|
||||
}
|
||||
|
||||
.toggleKnob {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
left: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 9999px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
||||
transition: transform $transition-normal;
|
||||
background-color: $white;
|
||||
|
||||
&.active {
|
||||
transform: translateX(1rem);
|
||||
}
|
||||
}
|
||||
|
||||
.toggleWrapper:has(.toggleKnob.active) .toggleTrack {
|
||||
background-color: $purple-600;
|
||||
}
|
||||
|
||||
.toggleInput {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.advancedSection {
|
||||
margin-top: 1rem;
|
||||
|
||||
&.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import {isNull, isUndefined} from '@abhijithvijayan/ts-utils';
|
||||
import {useFormState} from 'react-use-form-state';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import tw, {styled} from 'twin.macro';
|
||||
import {useState, useEffect, ChangeEvent} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import {useExtensionSettings} from '../contexts/extension-settings-context';
|
||||
import {updateExtensionSettings} from '../util/settings';
|
||||
@@ -18,6 +17,8 @@ import {
|
||||
|
||||
import Icon from '../components/Icon';
|
||||
|
||||
import styles from './Form.module.scss';
|
||||
|
||||
type OptionsFormValuesProperties = {
|
||||
apikey: string;
|
||||
history: boolean;
|
||||
@@ -25,22 +26,15 @@ type OptionsFormValuesProperties = {
|
||||
host: string;
|
||||
};
|
||||
|
||||
const StyledValidateButton = styled.button`
|
||||
${tw`focus:outline-none hover:text-gray-200 inline-flex items-center justify-center px-3 py-2 mt-3 mb-1 text-xs font-semibold text-center text-white duration-300 ease-in-out rounded shadow-lg`}
|
||||
type FormErrors = {
|
||||
apikey?: string;
|
||||
host?: string;
|
||||
};
|
||||
|
||||
background: linear-gradient(to right,rgb(126, 87, 194),rgb(98, 0, 234));
|
||||
|
||||
.validate__icon {
|
||||
${tw`inline-flex px-0 bg-transparent`}
|
||||
|
||||
svg {
|
||||
${tw`transition-transform duration-300 ease-in-out`}
|
||||
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
}
|
||||
}
|
||||
`;
|
||||
type FormValidity = {
|
||||
apikey?: boolean;
|
||||
host?: boolean;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onSave = (values: OptionsFormValuesProperties): Promise<any> => {
|
||||
@@ -48,7 +42,7 @@ const onSave = (values: OptionsFormValuesProperties): Promise<any> => {
|
||||
return updateExtensionSettings(values); // update local settings
|
||||
};
|
||||
|
||||
const Form: React.FC = () => {
|
||||
function Form() {
|
||||
const extensionSettingsState = useExtensionSettings()[0];
|
||||
const [submitting, setSubmitting] = useState<boolean>(false);
|
||||
const [showApiKey, setShowApiKey] = useState<boolean>(false);
|
||||
@@ -56,21 +50,8 @@ const Form: React.FC = () => {
|
||||
error: null,
|
||||
message: '',
|
||||
});
|
||||
const [
|
||||
formState,
|
||||
{
|
||||
text: textProps,
|
||||
checkbox: checkboxProps,
|
||||
password: passwordProps,
|
||||
label: labelProps,
|
||||
},
|
||||
] = useFormState<{
|
||||
apikey: string;
|
||||
history: boolean;
|
||||
advanced: boolean;
|
||||
host: string;
|
||||
}>(
|
||||
{
|
||||
|
||||
const [formValues, setFormValues] = useState<OptionsFormValuesProperties>({
|
||||
apikey: extensionSettingsState.apikey,
|
||||
history: extensionSettingsState.history,
|
||||
advanced: extensionSettingsState.advanced,
|
||||
@@ -78,68 +59,73 @@ const Form: React.FC = () => {
|
||||
(extensionSettingsState.advanced &&
|
||||
extensionSettingsState.host.hostUrl) ||
|
||||
'',
|
||||
},
|
||||
{
|
||||
withIds: true, // enable automatic creation of id and htmlFor props
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const {
|
||||
errors: formStateErrors,
|
||||
values: formStateValues,
|
||||
validity: formStateValidity,
|
||||
setField: setFormStateField,
|
||||
setFieldError: setFormStateFieldError,
|
||||
} = formState;
|
||||
const [formErrors, setFormErrors] = useState<FormErrors>({});
|
||||
const [formValidity, setFormValidity] = useState<FormValidity>({});
|
||||
|
||||
const isFormValid: boolean =
|
||||
((isUndefined(formStateValidity.apikey) || formStateValidity.apikey) &&
|
||||
formStateValues.apikey.trim().length === 40 && // invalidate if api key is empty
|
||||
isUndefined(formStateErrors.apikey) &&
|
||||
(((isUndefined(formStateValidity.host) || formStateValidity.host) &&
|
||||
isUndefined(formStateErrors.host)) ||
|
||||
// Check if `host` field exhibits validation errors, if `host` field is error but `advanced` field is set to false => form is valid(hence the or condition)
|
||||
!formStateValues.advanced)) ||
|
||||
((isUndefined(formValidity.apikey) || formValidity.apikey) &&
|
||||
formValues.apikey.trim().length === 40 &&
|
||||
isUndefined(formErrors.apikey) &&
|
||||
(((isUndefined(formValidity.host) || formValidity.host) &&
|
||||
isUndefined(formErrors.host)) ||
|
||||
!formValues.advanced)) ||
|
||||
false;
|
||||
|
||||
// on component mount -> save `settings` object
|
||||
useEffect(() => {
|
||||
onSave({
|
||||
...formStateValues,
|
||||
...(formStateValues.advanced === false && {host: ''}),
|
||||
...formValues,
|
||||
...(formValues.advanced === false && {host: ''}),
|
||||
});
|
||||
}, [formStateValues]);
|
||||
}, [formValues]);
|
||||
|
||||
function handleApiKeyInputChange(apikey: string): void {
|
||||
setFormStateField('apikey', apikey);
|
||||
// ToDo: Remove special symbols
|
||||
setFormValues((prev) => ({...prev, apikey}));
|
||||
|
||||
if (!(apikey.trim().length > 0)) {
|
||||
setFormStateFieldError('apikey', 'API key missing');
|
||||
setFormErrors((prev) => ({...prev, apikey: 'API key missing'}));
|
||||
setFormValidity((prev) => ({...prev, apikey: false}));
|
||||
} else if (apikey && apikey.trim().length < 40) {
|
||||
setFormStateFieldError('apikey', 'API key must be 40 characters');
|
||||
setFormErrors((prev) => ({...prev, apikey: 'API key must be 40 characters'}));
|
||||
setFormValidity((prev) => ({...prev, apikey: false}));
|
||||
} else if (apikey && apikey.trim().length > 40) {
|
||||
setFormStateFieldError('apikey', 'API key cannot exceed 40 characters');
|
||||
setFormErrors((prev) => ({...prev, apikey: 'API key cannot exceed 40 characters'}));
|
||||
setFormValidity((prev) => ({...prev, apikey: false}));
|
||||
} else {
|
||||
setFormErrors((prev) => {
|
||||
const {apikey: _, ...rest} = prev;
|
||||
return rest;
|
||||
});
|
||||
setFormValidity((prev) => ({...prev, apikey: true}));
|
||||
}
|
||||
}
|
||||
|
||||
function handleHostUrlInputChange(host: string): void {
|
||||
if (!formStateValues.advanced) {
|
||||
setFormStateFieldError('host', 'Enable Advanced Options first');
|
||||
|
||||
if (!formValues.advanced) {
|
||||
setFormErrors((prev) => ({...prev, host: 'Enable Advanced Options first'}));
|
||||
setFormValidity((prev) => ({...prev, host: false}));
|
||||
return;
|
||||
}
|
||||
|
||||
setFormStateField('host', host);
|
||||
setFormValues((prev) => ({...prev, host}));
|
||||
|
||||
if (!(host.trim().length > 0)) {
|
||||
setFormStateFieldError('host', 'Custom URL cannot be empty');
|
||||
|
||||
setFormErrors((prev) => ({...prev, host: 'Custom URL cannot be empty'}));
|
||||
setFormValidity((prev) => ({...prev, host: false}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidUrl(host.trim()) || host.trim().length < 10) {
|
||||
setFormStateFieldError('host', 'Please enter a valid url');
|
||||
setFormErrors((prev) => ({...prev, host: 'Please enter a valid url'}));
|
||||
setFormValidity((prev) => ({...prev, host: false}));
|
||||
} else {
|
||||
setFormErrors((prev) => {
|
||||
const {host: _, ...rest} = prev;
|
||||
return rest;
|
||||
});
|
||||
setFormValidity((prev) => ({...prev, host: true}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,11 +133,11 @@ const Form: React.FC = () => {
|
||||
setSubmitting(true);
|
||||
// request API validation request
|
||||
const apiKeyValidationBody: AuthRequestBodyProperties = {
|
||||
apikey: formStateValues.apikey.trim(),
|
||||
apikey: formValues.apikey.trim(),
|
||||
hostUrl:
|
||||
(formStateValues.advanced &&
|
||||
formStateValues.host.trim().length > 0 &&
|
||||
formStateValues.host.trim()) ||
|
||||
(formValues.advanced &&
|
||||
formValues.host.trim().length > 0 &&
|
||||
formValues.host.trim()) ||
|
||||
Kutt.hostUrl,
|
||||
};
|
||||
|
||||
@@ -185,67 +171,64 @@ const Form: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div tw="mt-4">
|
||||
<div tw="flex flex-col text-sm">
|
||||
<label {...labelProps('apikey')} tw="mb-2 font-bold">
|
||||
<div className={styles.formSection}>
|
||||
<div className={styles.inputGroup}>
|
||||
<label htmlFor="apikey" className={styles.label}>
|
||||
API Key
|
||||
<small tw="tracking-normal lowercase">
|
||||
<small>
|
||||
<a
|
||||
href={`${
|
||||
(formStateValues.advanced && formStateValues.host) ||
|
||||
(formValues.advanced && formValues.host) ||
|
||||
Kutt.hostUrl
|
||||
}/login`}
|
||||
target="blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
tw="ml-2 text-blue-500 no-underline"
|
||||
className={styles.labelLink}
|
||||
>
|
||||
get one?
|
||||
</a>
|
||||
</small>
|
||||
</label>
|
||||
|
||||
<div tw="relative">
|
||||
<div tw="absolute top-0 right-0 flex w-10 h-full border border-transparent">
|
||||
<div className={styles.inputWrapper}>
|
||||
<div className={styles.inputIconWrapper}>
|
||||
<Icon
|
||||
tw="z-10 cursor-pointer flex items-center justify-center w-full h-full text-gray-600 rounded-tl rounded-bl"
|
||||
className={styles.inputIcon}
|
||||
onClick={(): void => setShowApiKey(!showApiKey)}
|
||||
name={!showApiKey ? 'eye-closed' : 'eye'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
{...passwordProps('apikey')}
|
||||
id="apikey"
|
||||
name="apikey"
|
||||
type={!showApiKey ? 'password' : 'text'}
|
||||
onChange={({
|
||||
target: {value},
|
||||
}: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
// NOTE: overriding onChange to show errors
|
||||
handleApiKeyInputChange(value.trim());
|
||||
value={formValues.apikey}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
||||
handleApiKeyInputChange(e.target.value.trim());
|
||||
}}
|
||||
spellCheck="false"
|
||||
css={[
|
||||
tw`sm:text-base focus:border-indigo-400 focus:outline-none relative w-full py-2 pl-2 pr-12 text-sm placeholder-gray-400 bg-gray-200 border rounded`,
|
||||
|
||||
!isUndefined(formStateValidity.apikey) &&
|
||||
!formStateValidity.apikey &&
|
||||
tw`border-red-500`,
|
||||
]}
|
||||
className={clsx(
|
||||
styles.input,
|
||||
!isUndefined(formValidity.apikey) &&
|
||||
!formValidity.apikey &&
|
||||
styles.inputError
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span tw="flex items-center mt-1 ml-1 text-xs font-medium tracking-wide text-red-500">
|
||||
{formStateErrors.apikey}
|
||||
</span>
|
||||
<span className={styles.errorText}>{formErrors.apikey}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<StyledValidateButton
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || !isFormValid}
|
||||
onClick={handleApiKeyVerification}
|
||||
className={styles.validateButton}
|
||||
>
|
||||
<span tw="ml-2">Validate</span>
|
||||
<span className={styles.validateText}>Validate</span>
|
||||
|
||||
<Icon
|
||||
name={
|
||||
@@ -255,96 +238,93 @@ const Form: React.FC = () => {
|
||||
((!errored.error && 'tick') || 'cross')) ||
|
||||
'zap'
|
||||
}
|
||||
className="icon validate__icon"
|
||||
className={styles.validateIcon}
|
||||
/>
|
||||
</StyledValidateButton>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div tw="flex flex-col mt-6">
|
||||
<label
|
||||
{...labelProps('history')}
|
||||
tw="flex justify-between items-center mt-3 cursor-pointer"
|
||||
>
|
||||
<span tw="text-sm">Keep History</span>
|
||||
<div className={styles.toggleSection}>
|
||||
<label htmlFor="history" className={styles.toggleLabel}>
|
||||
<span className={styles.toggleText}>Keep History</span>
|
||||
|
||||
<span tw="relative ml-3">
|
||||
<span tw="block w-10 h-6 bg-gray-400 rounded-full shadow-inner" />
|
||||
<span className={styles.toggleWrapper}>
|
||||
<span className={styles.toggleTrack} />
|
||||
<span
|
||||
css={[
|
||||
tw`absolute inset-y-0 left-0 block w-4 h-4 mt-1 ml-1 transition-transform duration-300 ease-in-out rounded-full shadow`,
|
||||
|
||||
!formStateValues.history
|
||||
? tw`bg-white`
|
||||
: tw`transform translate-x-full bg-purple-600`,
|
||||
]}
|
||||
className={clsx(
|
||||
styles.toggleKnob,
|
||||
formValues.history && styles.active
|
||||
)}
|
||||
>
|
||||
<input
|
||||
{...checkboxProps('history')}
|
||||
tw="absolute w-0 h-0 opacity-0"
|
||||
id="history"
|
||||
name="history"
|
||||
type="checkbox"
|
||||
checked={formValues.history}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setFormValues((prev) => ({...prev, history: e.target.checked}));
|
||||
}}
|
||||
className={styles.toggleInput}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label
|
||||
{...labelProps('advanced')}
|
||||
tw="flex justify-between items-center mt-3 cursor-pointer"
|
||||
>
|
||||
<span tw="text-sm">Show Advanced Options</span>
|
||||
<label htmlFor="advanced" className={styles.toggleLabel}>
|
||||
<span className={styles.toggleText}>Show Advanced Options</span>
|
||||
|
||||
<span tw="relative ml-3">
|
||||
<span tw="block w-10 h-6 bg-gray-400 rounded-full shadow-inner" />
|
||||
<span className={styles.toggleWrapper}>
|
||||
<span className={styles.toggleTrack} />
|
||||
<span
|
||||
css={[
|
||||
tw`absolute inset-y-0 left-0 block w-4 h-4 mt-1 ml-1 transition-transform duration-300 ease-in-out rounded-full shadow`,
|
||||
|
||||
!formStateValues.advanced
|
||||
? tw`bg-white`
|
||||
: tw`transform translate-x-full bg-purple-600`,
|
||||
]}
|
||||
className={clsx(
|
||||
styles.toggleKnob,
|
||||
formValues.advanced && styles.active
|
||||
)}
|
||||
>
|
||||
<input
|
||||
{...checkboxProps('advanced')}
|
||||
tw="absolute w-0 h-0 opacity-0"
|
||||
id="advanced"
|
||||
name="advanced"
|
||||
type="checkbox"
|
||||
checked={formValues.advanced}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
||||
setFormValues((prev) => ({...prev, advanced: e.target.checked}));
|
||||
}}
|
||||
className={styles.toggleInput}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div css={[tw`mt-4`, !formStateValues.advanced && tw`invisible`]}>
|
||||
<div tw="flex flex-col text-sm">
|
||||
<label {...labelProps('host')} tw="mb-2 font-bold">
|
||||
<div className={clsx(styles.advancedSection, !formValues.advanced && styles.hidden)}>
|
||||
<div className={styles.inputGroup}>
|
||||
<label htmlFor="host" className={styles.label}>
|
||||
Custom Host
|
||||
</label>
|
||||
|
||||
<div tw="relative">
|
||||
<div className={styles.inputWrapper}>
|
||||
<input
|
||||
{...textProps('host')}
|
||||
onChange={({
|
||||
target: {value},
|
||||
}: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
// NOTE: overriding onChange to show errors
|
||||
handleHostUrlInputChange(value.trim());
|
||||
id="host"
|
||||
name="host"
|
||||
type="text"
|
||||
value={formValues.host}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
||||
handleHostUrlInputChange(e.target.value.trim());
|
||||
}}
|
||||
spellCheck="false"
|
||||
css={[
|
||||
tw`sm:text-base focus:border-indigo-400 focus:outline-none relative w-full py-2 pl-2 pr-12 text-sm placeholder-gray-400 bg-gray-200 border rounded`,
|
||||
|
||||
!isUndefined(formStateValidity.host) &&
|
||||
!formStateValidity.host &&
|
||||
tw`border-red-500`,
|
||||
]}
|
||||
className={clsx(
|
||||
styles.input,
|
||||
!isUndefined(formValidity.host) &&
|
||||
!formValidity.host &&
|
||||
styles.inputError
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span tw="flex items-center mt-1 ml-1 text-xs font-medium tracking-wide text-red-500">
|
||||
{formStateErrors.host}
|
||||
</span>
|
||||
<span className={styles.errorText}>{formErrors.host}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Form;
|
||||
|
||||
21
source/Options/Header.module.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: $medium;
|
||||
font-size: 1.875rem;
|
||||
margin-left: 0.25rem;
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
import React from 'react';
|
||||
import 'twin.macro';
|
||||
import {memo} from 'react';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
import styles from './Header.module.scss';
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<>
|
||||
<header tw="flex items-center justify-center pb-4">
|
||||
<header className={styles.header}>
|
||||
<img
|
||||
tw="w-8 h-8"
|
||||
className={styles.logo}
|
||||
width="32"
|
||||
height="32"
|
||||
src="assets/logo.png"
|
||||
src="../assets/logo.png"
|
||||
alt="logo"
|
||||
/>
|
||||
|
||||
<h1 tw="font-medium text-3xl ml-1 text-center mb-0">Kutt</h1>
|
||||
<h1 className={styles.title}>Kutt</h1>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default React.memo(Header);
|
||||
export default memo(Header);
|
||||
|
||||
27
source/Options/Options.module.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.optionsPage {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem 1.5rem;
|
||||
background-color: $gray-200;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.optionsContainer {
|
||||
max-width: 32rem;
|
||||
padding: 2.5rem 4rem;
|
||||
margin: 1.5rem 3rem;
|
||||
background-color: $white;
|
||||
height: max-content;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
border-radius: $radius-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.loaderContainer {
|
||||
height: 16rem;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import 'twin.macro';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
import {getExtensionSettings} from '../util/settings';
|
||||
import {
|
||||
@@ -20,7 +19,9 @@ import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
import Form from './Form';
|
||||
|
||||
const Options: React.FC = () => {
|
||||
import styles from './Options.module.scss';
|
||||
|
||||
function Options() {
|
||||
const extensionSettingsDispatch = useExtensionSettings()[1];
|
||||
const [requestStatusState, requestStatusDispatch] = useRequestStatus();
|
||||
|
||||
@@ -38,7 +39,7 @@ const Options: React.FC = () => {
|
||||
.replace('http://', '')
|
||||
.replace('https://', '')
|
||||
.replace('www.', '')
|
||||
.split(/[/?#]/)[0], // extract domain
|
||||
.split(/[/?#]/)[0] || '', // extract domain
|
||||
hostUrl: (settings.host as string).endsWith('/')
|
||||
? (settings.host as string).slice(0, -1)
|
||||
: (settings.host as string), // slice `/` at the end
|
||||
@@ -70,20 +71,14 @@ const Options: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<BodyWrapper>
|
||||
<div
|
||||
id="options"
|
||||
tw="h-screen flex justify-center px-6 py-8 bg-gray-200 select-none items-center"
|
||||
>
|
||||
<div
|
||||
tw="md:rounded-lg max-w-lg px-16 py-10 my-6 mx-12 bg-white"
|
||||
className={'h-max'}
|
||||
>
|
||||
<div id="options" className={styles.optionsPage}>
|
||||
<div className={styles.optionsContainer}>
|
||||
<Header />
|
||||
|
||||
{!requestStatusState.loading ? (
|
||||
<Form />
|
||||
) : (
|
||||
<div tw="h-64">
|
||||
<div className={styles.loaderContainer}>
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
@@ -94,6 +89,6 @@ const Options: React.FC = () => {
|
||||
</BodyWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Options;
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import {ThemeProvider} from 'styled-components';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from 'react';
|
||||
|
||||
// Common styles
|
||||
import '../styles/main.scss';
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
|
||||
import {ExtensionSettingsProvider} from '../contexts/extension-settings-context';
|
||||
import {RequestStatusProvider} from '../contexts/request-status-context';
|
||||
import Options from './Options';
|
||||
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax, import/no-unresolved, @typescript-eslint/no-var-requires, node/no-missing-require
|
||||
const theme = require('sass-extract-loader?{"plugins": ["sass-extract-js"]}!../styles/base/_variables.scss');
|
||||
// Require sass variables using sass-extract-loader and specify the plugin
|
||||
import '../styles/main.scss';
|
||||
|
||||
ReactDOM.render(
|
||||
<ThemeProvider theme={theme}>
|
||||
const container = document.getElementById('options-root');
|
||||
if (!container) {
|
||||
throw new Error('Could not find options-root container');
|
||||
}
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ExtensionSettingsProvider>
|
||||
<RequestStatusProvider>
|
||||
<Options />
|
||||
</RequestStatusProvider>
|
||||
</ExtensionSettingsProvider>
|
||||
</ThemeProvider>,
|
||||
document.getElementById('options-root')
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Options</title>
|
||||
<title>Options: Kutt</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="options-root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
186
source/Popup/Form.module.scss
Normal file
@@ -0,0 +1,186 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.formContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 24rem;
|
||||
padding: 1rem;
|
||||
margin: 0 auto;
|
||||
background-color: $white;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.formGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.formGroupRelative {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 0.75rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.025em;
|
||||
color: $gray-600;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
.labelAbsolute {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.025em;
|
||||
color: $gray-600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
background-color: $gray-200;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $radius-sm;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $indigo-400;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.selectOption {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
background-color: $gray-200;
|
||||
border: 1px solid transparent;
|
||||
border-radius: $radius-sm;
|
||||
margin-top: 1.2rem;
|
||||
|
||||
&::placeholder {
|
||||
color: $gray-400;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $indigo-400;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.inputError {
|
||||
border-color: $red-500;
|
||||
}
|
||||
|
||||
.errorText {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0.25rem;
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: $medium;
|
||||
letter-spacing: 0.025em;
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
.passwordWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.passwordToggle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
width: 2.5rem;
|
||||
margin-top: 1.75rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.passwordToggleIcon {
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px 0 0 4px;
|
||||
cursor: pointer;
|
||||
color: rgb(187, 187, 187);
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 0.25rem 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: $semibold;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
background: $primary-gradient;
|
||||
border: none;
|
||||
border-radius: $radius-sm;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
min-height: 36px;
|
||||
cursor: pointer;
|
||||
transition: color $transition-normal;
|
||||
|
||||
&:hover {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.createIcon {
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
|
||||
svg {
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
transition: transform $transition-normal;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import {useFormState} from 'react-use-form-state';
|
||||
import tw, {css, styled} from 'twin.macro';
|
||||
import React, {useState} from 'react';
|
||||
import {useState, type ChangeEvent} from 'react';
|
||||
import {
|
||||
EMPTY_STRING,
|
||||
isUndefined,
|
||||
isEmpty,
|
||||
isNull,
|
||||
get,
|
||||
} from '@abhijithvijayan/ts-utils';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import {useExtensionSettings} from '../contexts/extension-settings-context';
|
||||
import {SHORTEN_URL} from '../Background/constants';
|
||||
@@ -26,30 +24,13 @@ import {
|
||||
} from '../Background';
|
||||
|
||||
import Icon from '../components/Icon';
|
||||
import styles from './Form.module.scss';
|
||||
|
||||
export enum CONSTANTS {
|
||||
DefaultDomainId = 'default',
|
||||
}
|
||||
|
||||
const StyledValidateButton = styled.button`
|
||||
${tw`focus:outline-none hover:text-gray-200 inline-flex items-center justify-center w-full px-3 py-1 mb-1 text-xs font-semibold text-center text-white duration-300 ease-in-out rounded shadow-lg`}
|
||||
|
||||
background: linear-gradient(to right,rgb(126, 87, 194),rgb(98, 0, 234));
|
||||
min-height: 36px;
|
||||
|
||||
.create__icon {
|
||||
${tw`inline-flex px-0 bg-transparent`}
|
||||
|
||||
svg {
|
||||
${tw`transition-transform duration-300 ease-in-out`}
|
||||
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Form: React.FC = () => {
|
||||
function Form() {
|
||||
const extensionSettingsState = useExtensionSettings()[0];
|
||||
const requestStatusDispatch = useRequestStatus()[1];
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false);
|
||||
@@ -59,66 +40,33 @@ const Form: React.FC = () => {
|
||||
host: {hostDomain},
|
||||
} = extensionSettingsState;
|
||||
|
||||
const [
|
||||
formState,
|
||||
{
|
||||
text: textProps,
|
||||
password: passwordProps,
|
||||
select: selectProps,
|
||||
label: labelProps,
|
||||
},
|
||||
] = useFormState<{
|
||||
domain: string;
|
||||
customurl: string;
|
||||
password: string;
|
||||
}>(
|
||||
{
|
||||
const [formState, setFormState] = useState({
|
||||
domain:
|
||||
domainOptions
|
||||
.find(({id}) => {
|
||||
return id === CONSTANTS.DefaultDomainId;
|
||||
})
|
||||
?.value?.trim() || EMPTY_STRING, // empty string will map to disabled entry
|
||||
},
|
||||
{
|
||||
withIds: true, // enable automatic creation of id and htmlFor props
|
||||
}
|
||||
);
|
||||
const {
|
||||
errors: formStateErrors,
|
||||
validity: formStateValidity,
|
||||
setField: setFormStateField,
|
||||
setFieldError: setFormStateFieldError,
|
||||
} = formState;
|
||||
.find(({id}) => id === CONSTANTS.DefaultDomainId)
|
||||
?.value?.trim() || EMPTY_STRING,
|
||||
customurl: '',
|
||||
password: '',
|
||||
});
|
||||
const [formErrors, setFormErrors] = useState<{
|
||||
customurl?: string;
|
||||
password?: string;
|
||||
}>({});
|
||||
|
||||
const isFormValid: boolean =
|
||||
((isUndefined(formStateValidity.customurl) ||
|
||||
formStateValidity.customurl) &&
|
||||
(isUndefined(formStateValidity.password) || formStateValidity.password) &&
|
||||
isUndefined(formStateErrors.customurl) &&
|
||||
isUndefined(formStateErrors.password)) ||
|
||||
false;
|
||||
!formErrors.customurl &&
|
||||
!formErrors.password &&
|
||||
true;
|
||||
|
||||
async function handleFormSubmit({
|
||||
customurl,
|
||||
password,
|
||||
domain,
|
||||
}: {
|
||||
domain: string;
|
||||
customurl: string;
|
||||
password: string;
|
||||
}): Promise<void> {
|
||||
// enable loading screen
|
||||
async function handleFormSubmit(): Promise<void> {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Get target link to shorten
|
||||
const tabs = await getCurrentTab();
|
||||
const target: string | null = get(tabs, '[0].url', null);
|
||||
const shouldSubmit: boolean = !isNull(target) && isValidUrl(target);
|
||||
|
||||
if (!shouldSubmit) {
|
||||
setIsSubmitting(false);
|
||||
|
||||
requestStatusDispatch({
|
||||
type: RequestStatusActionTypes.SET_REQUEST_STATUS,
|
||||
payload: {
|
||||
@@ -126,35 +74,36 @@ const Form: React.FC = () => {
|
||||
message: 'Not a valid URL',
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const apiBody: ApiBodyProperties = {
|
||||
apikey: extensionSettingsState.apikey,
|
||||
target: target as unknown as string,
|
||||
...(customurl.trim() !== EMPTY_STRING && {customurl: customurl.trim()}), // add key only if field is not empty
|
||||
...(!isEmpty(password) && {password}),
|
||||
...(formState.customurl.trim() !== EMPTY_STRING && {
|
||||
customurl: formState.customurl.trim(),
|
||||
}),
|
||||
...(!isEmpty(formState.password) && {password: formState.password}),
|
||||
reuse: false,
|
||||
...(domain.trim() !== EMPTY_STRING && {domain: domain.trim()}),
|
||||
...(formState.domain.trim() !== EMPTY_STRING && {
|
||||
domain: formState.domain.trim(),
|
||||
}),
|
||||
};
|
||||
|
||||
const apiShortenUrlBody: ShortUrlActionBodyProperties = {
|
||||
apiBody,
|
||||
hostUrl: extensionSettingsState.host.hostUrl,
|
||||
};
|
||||
// shorten url in the background
|
||||
|
||||
const response: SuccessfulShortenStatusProperties | ApiErroredProperties =
|
||||
await messageUtil.send(SHORTEN_URL, apiShortenUrlBody);
|
||||
|
||||
// disable spinner
|
||||
setIsSubmitting(false);
|
||||
|
||||
if (!response.error) {
|
||||
const {
|
||||
data: {link},
|
||||
} = response;
|
||||
// show shortened url
|
||||
requestStatusDispatch({
|
||||
type: RequestStatusActionTypes.SET_REQUEST_STATUS,
|
||||
payload: {
|
||||
@@ -163,7 +112,6 @@ const Form: React.FC = () => {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// errored
|
||||
requestStatusDispatch({
|
||||
type: RequestStatusActionTypes.SET_REQUEST_STATUS,
|
||||
payload: {
|
||||
@@ -175,118 +123,89 @@ const Form: React.FC = () => {
|
||||
}
|
||||
|
||||
function handleCustomUrlInputChange(url: string): void {
|
||||
setFormStateField('customurl', url);
|
||||
// ToDo: Remove special symbols
|
||||
|
||||
setFormState((prev) => ({...prev, customurl: url}));
|
||||
if (url.length > 0 && url.length < 3) {
|
||||
setFormStateFieldError(
|
||||
'customurl',
|
||||
'Custom URL must be at-least 3 characters'
|
||||
);
|
||||
setFormErrors((prev) => ({
|
||||
...prev,
|
||||
customurl: 'Custom URL must be at-least 3 characters',
|
||||
}));
|
||||
} else {
|
||||
setFormErrors((prev) => ({...prev, customurl: undefined}));
|
||||
}
|
||||
}
|
||||
|
||||
function handlePasswordInputChange(password: string): void {
|
||||
setFormStateField('password', password);
|
||||
// ToDo: Remove special symbols
|
||||
|
||||
setFormState((prev) => ({...prev, password}));
|
||||
if (password.length > 0 && password.length < 3) {
|
||||
setFormStateFieldError(
|
||||
'password',
|
||||
'Password must be at-least 3 characters'
|
||||
);
|
||||
setFormErrors((prev) => ({
|
||||
...prev,
|
||||
password: 'Password must be at-least 3 characters',
|
||||
}));
|
||||
} else {
|
||||
setFormErrors((prev) => ({...prev, password: undefined}));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div tw="flex flex-col w-full max-w-sm p-4 mx-auto bg-white select-none">
|
||||
<div tw="flex flex-col mb-4">
|
||||
<label
|
||||
{...labelProps('domain')}
|
||||
tw="sm:text-sm mb-1 text-xs tracking-wide text-gray-600"
|
||||
>
|
||||
<div className={styles.formContainer}>
|
||||
<div className={styles.formGroup}>
|
||||
<label htmlFor="domain" className={styles.label}>
|
||||
Domain
|
||||
</label>
|
||||
|
||||
<div tw="relative">
|
||||
<div className={styles.selectWrapper}>
|
||||
<select
|
||||
{...selectProps('domain')}
|
||||
id="domain"
|
||||
name="domain"
|
||||
value={formState.domain}
|
||||
onChange={(e) =>
|
||||
setFormState((prev) => ({...prev, domain: e.target.value}))
|
||||
}
|
||||
disabled={isSubmitting}
|
||||
css={[
|
||||
tw`sm:text-base focus:border-indigo-400 focus:outline-none relative w-full px-2 py-2 text-sm placeholder-gray-400 bg-gray-200 border rounded`,
|
||||
]}
|
||||
className={styles.select}
|
||||
>
|
||||
{domainOptions.map(({id, option, value, disabled = false}) => {
|
||||
return (
|
||||
{domainOptions.map(({id, option, value, disabled = false}) => (
|
||||
<option
|
||||
tw="bg-gray-200 "
|
||||
className={styles.selectOption}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
key={id}
|
||||
>
|
||||
{option}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div tw="flex flex-col mb-3 relative">
|
||||
<label
|
||||
{...labelProps('customurl')}
|
||||
tw="sm:text-sm absolute top-0 bottom-0 left-0 right-0 z-10 block text-xs tracking-wide text-gray-600 cursor-pointer"
|
||||
>
|
||||
<div className={styles.formGroupRelative}>
|
||||
<label htmlFor="customurl" className={styles.labelAbsolute}>
|
||||
<span>{hostDomain}/</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
{...textProps('customurl')}
|
||||
onChange={({
|
||||
target: {value},
|
||||
}: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
// NOTE: overriding onChange to show errors
|
||||
handleCustomUrlInputChange(value.trim());
|
||||
id="customurl"
|
||||
name="customurl"
|
||||
type="text"
|
||||
value={formState.customurl}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
||||
handleCustomUrlInputChange(e.target.value.trim());
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
spellCheck="false"
|
||||
css={[
|
||||
tw`focus:outline-none sm:text-base focus:border-indigo-400 w-full px-2 py-2 text-sm placeholder-gray-400 bg-gray-200 border rounded`,
|
||||
|
||||
css`
|
||||
margin-top: 1.2rem;
|
||||
`,
|
||||
|
||||
!isUndefined(formStateValidity.customurl) &&
|
||||
!formStateValidity.customurl &&
|
||||
tw`border-red-500`,
|
||||
]}
|
||||
className={clsx(styles.input, formErrors.customurl && styles.inputError)}
|
||||
/>
|
||||
|
||||
<span tw="flex items-center mt-1 ml-1 text-xs font-medium tracking-wide text-red-500">
|
||||
{formStateErrors.customurl}
|
||||
</span>
|
||||
<span className={styles.errorText}>{formErrors.customurl}</span>
|
||||
</div>
|
||||
|
||||
<div tw="flex flex-col mb-3 relative">
|
||||
<label
|
||||
{...labelProps('password')}
|
||||
tw="sm:text-sm absolute top-0 bottom-0 left-0 right-0 z-10 block text-xs tracking-wide text-gray-600 cursor-pointer"
|
||||
>
|
||||
<div className={styles.formGroupRelative}>
|
||||
<label htmlFor="password" className={styles.labelAbsolute}>
|
||||
<span>Password</span>
|
||||
</label>
|
||||
|
||||
<div tw="relative">
|
||||
<div
|
||||
css={[
|
||||
tw`absolute top-0 right-0 flex w-10 mt-6 border border-transparent`,
|
||||
|
||||
css`
|
||||
margin-top: 1.75rem;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<div className={styles.passwordWrapper}>
|
||||
<div className={styles.passwordToggle}>
|
||||
<Icon
|
||||
onClick={(): void => {
|
||||
if (!isSubmitting) {
|
||||
@@ -294,61 +213,41 @@ const Form: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
name={!showPassword ? 'eye-closed' : 'eye'}
|
||||
css={[
|
||||
tw`z-10 flex items-center justify-center w-full h-full rounded-tl rounded-bl cursor-pointer`,
|
||||
|
||||
css`
|
||||
color: rgb(187, 187, 187);
|
||||
`,
|
||||
]}
|
||||
className={styles.passwordToggleIcon}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
{...passwordProps('password')}
|
||||
id="password"
|
||||
name="password"
|
||||
type={!showPassword ? 'password' : 'text'}
|
||||
value={formState.password}
|
||||
spellCheck="false"
|
||||
onChange={({
|
||||
target: {value},
|
||||
}: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
// NOTE: overriding onChange to show errors
|
||||
handlePasswordInputChange(value);
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
|
||||
handlePasswordInputChange(e.target.value);
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
css={[
|
||||
tw`focus:outline-none sm:text-base focus:border-indigo-400 w-full px-2 py-2 text-sm placeholder-gray-400 bg-gray-200 border rounded`,
|
||||
|
||||
css`
|
||||
margin-top: 1.2rem;
|
||||
`,
|
||||
|
||||
!isUndefined(formStateValidity.password) &&
|
||||
!formStateValidity.password &&
|
||||
tw`border-red-500`,
|
||||
]}
|
||||
className={clsx(styles.input, formErrors.password && styles.inputError)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span tw="flex items-center mt-1 ml-1 text-xs font-medium tracking-wide text-red-500">
|
||||
{formStateErrors.password}
|
||||
</span>
|
||||
<span className={styles.errorText}>{formErrors.password}</span>
|
||||
</div>
|
||||
|
||||
<StyledValidateButton
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isFormValid || isSubmitting}
|
||||
onClick={(): void => {
|
||||
handleFormSubmit(formState.values);
|
||||
}}
|
||||
onClick={handleFormSubmit}
|
||||
className={styles.submitButton}
|
||||
>
|
||||
{!isSubmitting ? (
|
||||
<span>Create</span>
|
||||
) : (
|
||||
<Icon className="icon create__icon" name="spinner" />
|
||||
<Icon className={styles.createIcon} name="spinner" />
|
||||
)}
|
||||
</StyledValidateButton>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Form;
|
||||
|
||||
28
source/Popup/Header.module.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.styledIcon {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
color: rgb(187, 187, 187);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {isNull, EMPTY_STRING} from '@abhijithvijayan/ts-utils';
|
||||
import React, {useState} from 'react';
|
||||
import tw, {styled} from 'twin.macro';
|
||||
import {useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import {openExtOptionsPage, openHistoryPage} from '../util/tabs';
|
||||
import {updateExtensionSettings} from '../util/settings';
|
||||
@@ -18,14 +18,9 @@ import {
|
||||
} from '../Background';
|
||||
|
||||
import Icon from '../components/Icon';
|
||||
import styles from './Header.module.scss';
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
${tw`hover:opacity-75 bg-transparent shadow-none`}
|
||||
|
||||
color: rgb(187, 187, 187);
|
||||
`;
|
||||
|
||||
const Header: React.FC = () => {
|
||||
function Header() {
|
||||
const [extensionSettingsState, extensionSettingsDispatch] =
|
||||
useExtensionSettings();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
@@ -35,7 +30,6 @@ const Header: React.FC = () => {
|
||||
});
|
||||
|
||||
async function fetchUserDomains(): Promise<void> {
|
||||
// show loading spinner
|
||||
setLoading(true);
|
||||
|
||||
const apiKeyValidationBody: AuthRequestBodyProperties = {
|
||||
@@ -43,36 +37,26 @@ const Header: React.FC = () => {
|
||||
hostUrl: extensionSettingsState.host.hostUrl,
|
||||
};
|
||||
|
||||
// request API
|
||||
const response: SuccessfulApiKeyCheckProperties | ApiErroredProperties =
|
||||
await messageUtil.send(CHECK_API_KEY, apiKeyValidationBody);
|
||||
|
||||
// stop spinner
|
||||
setLoading(false);
|
||||
|
||||
if (!response.error) {
|
||||
// ---- success ---- //
|
||||
setErrored({error: false, message: 'Fetching domains successful'});
|
||||
|
||||
// Store user account information
|
||||
const {domains, email} = response.data;
|
||||
await updateExtensionSettings({user: {domains, email}});
|
||||
} else {
|
||||
// ---- errored ---- //
|
||||
setErrored({error: true, message: response.message});
|
||||
|
||||
// Delete `user` field from settings
|
||||
await updateExtensionSettings({user: null});
|
||||
}
|
||||
|
||||
// hot reload page(read from localstorage and update state)
|
||||
extensionSettingsDispatch({
|
||||
type: ExtensionSettingsActionTypes.RELOAD_EXTENSION_SETTINGS,
|
||||
payload: !extensionSettingsState.reload,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
// Reset status
|
||||
setErrored({error: null, message: EMPTY_STRING});
|
||||
}, 1000);
|
||||
}
|
||||
@@ -83,43 +67,41 @@ const Header: React.FC = () => {
|
||||
'refresh';
|
||||
|
||||
return (
|
||||
<>
|
||||
<header tw="flex items-center justify-between p-4 select-none">
|
||||
<header className={styles.header}>
|
||||
<div>
|
||||
<img
|
||||
tw="w-8 h-8"
|
||||
className={styles.logo}
|
||||
width="32"
|
||||
height="32"
|
||||
src="assets/logo.png"
|
||||
src="../assets/logo.png"
|
||||
alt="logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div tw="flex">
|
||||
<StyledIcon
|
||||
<div className={styles.actions}>
|
||||
<Icon
|
||||
onClick={fetchUserDomains}
|
||||
name={iconToShow}
|
||||
title="Refresh"
|
||||
className="icon"
|
||||
className={clsx('icon', styles.styledIcon)}
|
||||
/>
|
||||
{extensionSettingsState.history && (
|
||||
<StyledIcon
|
||||
<Icon
|
||||
onClick={openHistoryPage}
|
||||
name="clock"
|
||||
className="icon"
|
||||
className={clsx('icon', styles.styledIcon)}
|
||||
title="History"
|
||||
/>
|
||||
)}
|
||||
<StyledIcon
|
||||
<Icon
|
||||
onClick={openExtOptionsPage}
|
||||
name="settings"
|
||||
className="icon"
|
||||
className={clsx('icon', styles.styledIcon)}
|
||||
title="Settings"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Header;
|
||||
|
||||
7
source/Popup/Popup.module.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.popup {
|
||||
font-size: 1.125rem;
|
||||
min-height: 350px;
|
||||
min-width: 270px;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import {isNull, EMPTY_STRING} from '@abhijithvijayan/ts-utils';
|
||||
import React, {useEffect} from 'react';
|
||||
import tw, {css} from 'twin.macro';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
import {Kutt, UserSettingsResponseProperties} from '../Background';
|
||||
import {openExtOptionsPage} from '../util/tabs';
|
||||
@@ -28,7 +27,9 @@ import PopupHeader from './Header';
|
||||
import Loader from '../components/Loader';
|
||||
import Form, {CONSTANTS} from './Form';
|
||||
|
||||
const Popup: React.FC = () => {
|
||||
import styles from './Popup.module.scss';
|
||||
|
||||
function Popup() {
|
||||
const [extensionSettingsState, extensionSettingsDispatch] =
|
||||
useExtensionSettings();
|
||||
const [requestStatusState, requestStatusDispatch] = useRequestStatus();
|
||||
@@ -37,12 +38,10 @@ const Popup: React.FC = () => {
|
||||
// re-renders on `liveReloadFlag` change
|
||||
useEffect((): void => {
|
||||
async function getUserSettings(): Promise<void> {
|
||||
// -----------------------------------------------------------------------------//
|
||||
// -----------------------------------------------------------------------------//
|
||||
// ----- // ToDo: remove in next major release // ----- //
|
||||
// ----- Ref: https://github.com/thedevs-network/kutt-extension/issues/78 ----- //
|
||||
// -----------------------------------------------------------------------------//
|
||||
// -----------------------------------------------------------------------------//
|
||||
|
||||
const {
|
||||
// old keys from extension v3.x.x
|
||||
@@ -61,7 +60,6 @@ const Popup: React.FC = () => {
|
||||
let performMigration = false;
|
||||
|
||||
if ((key as string).trim().length > 0) {
|
||||
// map it to `settings.apikey`
|
||||
migrationSettings.apikey = key;
|
||||
performMigration = true;
|
||||
}
|
||||
@@ -69,30 +67,20 @@ const Popup: React.FC = () => {
|
||||
(host as string).trim().length > 0 &&
|
||||
(userOptions.devMode as boolean)
|
||||
) {
|
||||
// map `host` to `settings.host`
|
||||
migrationSettings.host = host;
|
||||
// set `advanced` to true
|
||||
migrationSettings.advanced = true;
|
||||
performMigration = true;
|
||||
}
|
||||
if (userOptions.keepHistory as boolean) {
|
||||
// set `settings.history` to true
|
||||
migrationSettings.history = true;
|
||||
performMigration = true;
|
||||
}
|
||||
if (performMigration) {
|
||||
// perform migration
|
||||
await migrateSettings(migrationSettings);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------//
|
||||
// -----------------------------------------------------------------------------//
|
||||
// -----------------------------------------------------------------------------//
|
||||
// -----------------------------------------------------------------------------//
|
||||
// -----------------------------------------------------------------------------//
|
||||
// -----------------------------------------------------------------------------//
|
||||
|
||||
// ToDo: set types: refer https://kutt.it/jITyIU
|
||||
const {settings = {}} = await getExtensionSettings();
|
||||
|
||||
// No API Key set
|
||||
@@ -138,16 +126,15 @@ const Popup: React.FC = () => {
|
||||
.replace('http://', EMPTY_STRING)
|
||||
.replace('https://', EMPTY_STRING)
|
||||
.replace('www.', EMPTY_STRING)
|
||||
.split(/[/?#]/)[0], // extract domain
|
||||
.split(/[/?#]/)[0] || EMPTY_STRING,
|
||||
hostUrl: (settings.host as string).endsWith('/')
|
||||
? (settings.host as string).slice(0, -1)
|
||||
: (settings.host as string), // slice `/` at the end
|
||||
: (settings.host as string),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let historyEnabled = false;
|
||||
// `history` field set
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(settings, 'history') &&
|
||||
(settings.history as boolean)
|
||||
@@ -189,10 +176,8 @@ const Popup: React.FC = () => {
|
||||
}
|
||||
);
|
||||
|
||||
// merge to beginning of array
|
||||
optionsList = defaultOptions.concat(optionsList);
|
||||
|
||||
// update domain list
|
||||
extensionSettingsDispatch({
|
||||
type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS,
|
||||
payload: {
|
||||
@@ -203,7 +188,6 @@ const Popup: React.FC = () => {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// no `user` but `apikey` exist on storage
|
||||
extensionSettingsDispatch({
|
||||
type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS,
|
||||
payload: {
|
||||
@@ -215,7 +199,6 @@ const Popup: React.FC = () => {
|
||||
});
|
||||
}
|
||||
|
||||
// stop loader
|
||||
requestStatusDispatch({
|
||||
type: RequestStatusActionTypes.SET_LOADING,
|
||||
payload: false,
|
||||
@@ -227,16 +210,7 @@ const Popup: React.FC = () => {
|
||||
|
||||
return (
|
||||
<BodyWrapper>
|
||||
<div
|
||||
id="popup"
|
||||
css={[
|
||||
tw`text-lg`,
|
||||
css`
|
||||
min-height: 350px;
|
||||
min-width: 270px;
|
||||
`,
|
||||
]}
|
||||
>
|
||||
<div id="popup" className={styles.popup}>
|
||||
{!requestStatusState.loading ? (
|
||||
<>
|
||||
<PopupHeader />
|
||||
@@ -249,6 +223,6 @@ const Popup: React.FC = () => {
|
||||
</div>
|
||||
</BodyWrapper>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Popup;
|
||||
|
||||
54
source/Popup/ResponseBody.module.scss
Normal file
@@ -0,0 +1,54 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.popupBody {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem 1rem 0;
|
||||
|
||||
.icon {
|
||||
svg {
|
||||
stroke: rgb(101, 189, 137);
|
||||
stroke-width: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qrIcon {
|
||||
margin: 0;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
.copyIcon {
|
||||
margin: 0;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
border-bottom: 1px dotted $stats-total-underline;
|
||||
padding-bottom: 2px;
|
||||
color: rgb(41, 71, 86);
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: $light;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
padding-top: 0.25rem;
|
||||
font-size: 1.125rem;
|
||||
color: $gray-900;
|
||||
border-bottom: 1px dotted $gray-700;
|
||||
}
|
||||
|
||||
.qrCodeContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
padding: 1rem 0 0;
|
||||
}
|
||||
@@ -1,44 +1,27 @@
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import tw, {styled, css} from 'twin.macro';
|
||||
import QRCode from 'qrcode.react';
|
||||
import {useState, useEffect} from 'react';
|
||||
import {QRCodeSVG} from 'qrcode.react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import {useRequestStatus} from '../contexts/request-status-context';
|
||||
import {removeProtocol} from '../util/link';
|
||||
import Icon from '../components/Icon';
|
||||
|
||||
import styles from './ResponseBody.module.scss';
|
||||
|
||||
export type ProcessedRequestProperties = {
|
||||
error: boolean | null;
|
||||
message: string;
|
||||
};
|
||||
|
||||
const StyledPopupBody = styled.div`
|
||||
${tw`flex items-center justify-center px-4 pt-4 pb-0`}
|
||||
|
||||
.icon {
|
||||
svg {
|
||||
stroke: rgb(101, 189, 137);
|
||||
stroke-width: 2;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
border-bottom: 1px dotted ${({theme}): string => theme.statsTotalUnderline};
|
||||
padding-bottom: 2px;
|
||||
color: rgb(41, 71, 86);
|
||||
|
||||
${tw`hover:opacity-75 min-w-0 m-0 text-2xl font-light cursor-pointer`}
|
||||
}
|
||||
`;
|
||||
|
||||
const ResponseBody: React.FC = () => {
|
||||
function ResponseBody() {
|
||||
const [{error, message}] = useRequestStatus();
|
||||
const [copied, setCopied] = useState<boolean>(false);
|
||||
const [QRView, setQRView] = useState<boolean>(false);
|
||||
|
||||
// reset copy message
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
timer = setTimeout(() => {
|
||||
setCopied(false);
|
||||
@@ -53,18 +36,11 @@ const ResponseBody: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledPopupBody>
|
||||
<div className={styles.popupBody}>
|
||||
{!error ? (
|
||||
<>
|
||||
<Icon
|
||||
className="icon"
|
||||
css={[
|
||||
tw`my-0 ml-0`,
|
||||
|
||||
css`
|
||||
margin-right: 0.4rem;
|
||||
`,
|
||||
]}
|
||||
className={clsx(styles.icon, styles.qrIcon)}
|
||||
name="qrcode"
|
||||
onClick={(): void => {
|
||||
return setQRView(!QRView);
|
||||
@@ -78,10 +54,10 @@ const ResponseBody: React.FC = () => {
|
||||
return setCopied(true);
|
||||
}}
|
||||
>
|
||||
<Icon tw="my-0 ml-0 mr-3" className="icon" name="copy" />
|
||||
<Icon className={clsx(styles.icon, styles.copyIcon)} name="copy" />
|
||||
</CopyToClipboard>
|
||||
) : (
|
||||
<Icon tw="my-0 ml-0 mr-3" className="icon" name="tick" />
|
||||
<Icon className={clsx(styles.icon, styles.copyIcon)} name="tick" />
|
||||
)}
|
||||
|
||||
<CopyToClipboard
|
||||
@@ -90,23 +66,21 @@ const ResponseBody: React.FC = () => {
|
||||
return setCopied(true);
|
||||
}}
|
||||
>
|
||||
<h1>{removeProtocol(message)}</h1>
|
||||
<h1 className={styles.link}>{removeProtocol(message)}</h1>
|
||||
</CopyToClipboard>
|
||||
</>
|
||||
) : (
|
||||
<p tw="pt-1 text-lg text-gray-900 border-b border-gray-700 border-dotted">
|
||||
{message}
|
||||
</p>
|
||||
<p className={styles.errorMessage}>{message}</p>
|
||||
)}
|
||||
</StyledPopupBody>
|
||||
</div>
|
||||
|
||||
{!error && QRView && (
|
||||
<div tw="flex justify-center max-w-full pt-4 pb-0">
|
||||
<QRCode size={128} value={message} />
|
||||
<div className={styles.qrCodeContainer}>
|
||||
<QRCodeSVG size={128} value={message} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default ResponseBody;
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import {ThemeProvider} from 'styled-components';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
// Common styles
|
||||
import '../styles/main.scss';
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
|
||||
import {ExtensionSettingsProvider} from '../contexts/extension-settings-context';
|
||||
import {RequestStatusProvider} from '../contexts/request-status-context';
|
||||
import Popup from './Popup';
|
||||
|
||||
// eslint-disable-next-line import/no-webpack-loader-syntax, import/no-unresolved, @typescript-eslint/no-var-requires, node/no-missing-require
|
||||
const theme = require('sass-extract-loader?{"plugins": ["sass-extract-js"]}!../styles/base/_variables.scss');
|
||||
// Require sass variables using sass-extract-loader and specify the plugin
|
||||
import '../styles/main.scss';
|
||||
|
||||
ReactDOM.render(
|
||||
<ThemeProvider theme={theme}>
|
||||
const container = document.getElementById('popup-root');
|
||||
if (!container) {
|
||||
throw new Error('Could not find popup-root container');
|
||||
}
|
||||
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ExtensionSettingsProvider>
|
||||
<RequestStatusProvider>
|
||||
<Popup />
|
||||
</RequestStatusProvider>
|
||||
</ExtensionSettingsProvider>
|
||||
</ThemeProvider>,
|
||||
document.getElementById('popup-root')
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=500" />
|
||||
<title>Popup</title>
|
||||
<title>Kutt</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="popup-root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3
source/components/BodyWrapper.module.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,19 +1,12 @@
|
||||
import React from 'react';
|
||||
import 'twin.macro';
|
||||
import type {ReactNode} from 'react';
|
||||
import styles from './BodyWrapper.module.scss';
|
||||
|
||||
type WrapperProperties = {
|
||||
children: React.ReactChild;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const BodyWrapper: React.FC<WrapperProperties> = ({children}) => {
|
||||
// ToDo: get from props
|
||||
const isLoading = false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div tw="w-full">{isLoading ? 'Loading...' : children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
function BodyWrapper({children}: WrapperProperties) {
|
||||
return <div className={styles.wrapper}>{children}</div>;
|
||||
}
|
||||
|
||||
export default BodyWrapper;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
const Spinner: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
10
source/components/Loader.module.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.loader {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
@@ -1,25 +1,12 @@
|
||||
import React from 'react';
|
||||
import tw, {css} from 'twin.macro';
|
||||
|
||||
import Icon from './Icon';
|
||||
import styles from './Loader.module.scss';
|
||||
|
||||
const Loader: React.FC = (props) => {
|
||||
function Loader() {
|
||||
return (
|
||||
<div
|
||||
css={[
|
||||
tw`fixed flex items-center justify-center h-full`,
|
||||
|
||||
css`
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
`,
|
||||
]}
|
||||
{...props}
|
||||
>
|
||||
<div className={styles.loader}>
|
||||
<Icon name="spinner" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default Loader;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import React, {createContext, useReducer, useContext} from 'react';
|
||||
import {createContext, useReducer, useContext, type ReactNode} from 'react';
|
||||
|
||||
import {Kutt} from '../Background';
|
||||
|
||||
@@ -116,12 +116,12 @@ function useExtensionSettings(): [State, Dispatch] {
|
||||
}
|
||||
|
||||
type ExtensionSettingsProviderProps = {
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const ExtensionSettingsProvider: React.FC<ExtensionSettingsProviderProps> = ({
|
||||
function ExtensionSettingsProvider({
|
||||
children,
|
||||
}) => {
|
||||
}: ExtensionSettingsProviderProps) {
|
||||
const [state, dispatch] = useReducer(extensionSettingsReducer, initialValues);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import React, {createContext, useReducer, useContext} from 'react';
|
||||
import {createContext, useReducer, useContext, type ReactNode} from 'react';
|
||||
|
||||
export enum RequestStatusActionTypes {
|
||||
SET_REQUEST_STATUS = 'set-request-status',
|
||||
@@ -86,12 +86,10 @@ function useRequestStatus(): [State, Dispatch] {
|
||||
}
|
||||
|
||||
type RequestStatusProviderProps = {
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const RequestStatusProvider: React.FC<RequestStatusProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
function RequestStatusProvider({children}: RequestStatusProviderProps) {
|
||||
const [state, dispatch] = useReducer(requestStatusReducer, initialValues);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import React, {createContext, useContext, useReducer} from 'react';
|
||||
import {createContext, useContext, useReducer, type ReactNode} from 'react';
|
||||
|
||||
import {UserShortenedLinkStats} from '../Background';
|
||||
|
||||
@@ -92,7 +92,11 @@ function useShortenedLinks(): [State, Dispatch] {
|
||||
return [useShortenedLinksContextState(), useShortenedLinksContextDispatch()];
|
||||
}
|
||||
|
||||
const ShortenedLinksProvider: React.FC = ({children}) => {
|
||||
type ShortenedLinksProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
function ShortenedLinksProvider({children}: ShortenedLinksProviderProps) {
|
||||
const [state, dispatch] = useReducer(shortenedLinksReducer, initialValues);
|
||||
|
||||
return (
|
||||
|
||||
12
source/globals.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
declare const __DEV__: boolean;
|
||||
declare const __TARGET_BROWSER__: 'chrome' | 'firefox' | 'opera';
|
||||
|
||||
declare module '*.module.scss' {
|
||||
const classes: {readonly [key: string]: string};
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*.scss' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
@@ -1,67 +1,58 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"manifest_version": 3,
|
||||
"name": "Kutt",
|
||||
"version": "0.0.0",
|
||||
"short_name": "Kutt",
|
||||
"description": "Shorten long URLs with just one click.",
|
||||
"icons": {
|
||||
"16": "assets/favicon-16.png",
|
||||
"32": "assets/favicon-32.png",
|
||||
"48": "assets/favicon-48.png",
|
||||
"128": "assets/favicon-128.png"
|
||||
"16": "assets/icons/favicon-16.png",
|
||||
"32": "assets/icons/favicon-32.png",
|
||||
"48": "assets/icons/favicon-48.png",
|
||||
"128": "assets/icons/favicon-128.png"
|
||||
},
|
||||
"homepage_url": "https://github.com/thedevs-network/kutt-extension.git",
|
||||
"__firefox__browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "support@kutt.it",
|
||||
"strict_min_version": "52.0"
|
||||
"strict_min_version": "109.0"
|
||||
}
|
||||
},
|
||||
"__chrome|firefox__author": "abhijithvijayan",
|
||||
"__opera__developer": {
|
||||
"name": "abhijithvijayan"
|
||||
},
|
||||
"browser_action": {
|
||||
"default_popup": "popup.html",
|
||||
"action": {
|
||||
"default_popup": "Popup/popup.html",
|
||||
"default_icon": {
|
||||
"16": "assets/favicon-16.png",
|
||||
"32": "assets/favicon-32.png",
|
||||
"48": "assets/favicon-48.png",
|
||||
"128": "assets/favicon-128.png"
|
||||
"16": "assets/icons/favicon-16.png",
|
||||
"32": "assets/icons/favicon-32.png",
|
||||
"48": "assets/icons/favicon-48.png",
|
||||
"128": "assets/icons/favicon-128.png"
|
||||
},
|
||||
"default_title": "Shorten this URL",
|
||||
"__chrome|opera__chrome_style": false,
|
||||
"__firefox__browser_style": false
|
||||
"default_title": "Shorten this URL"
|
||||
},
|
||||
"background": {
|
||||
"__chrome|opera__persistent": false,
|
||||
"scripts": [
|
||||
"js/background.bundle.js"
|
||||
]
|
||||
"__chrome|opera__service_worker": "assets/js/background.bundle.js",
|
||||
"__chrome|opera__type": "module",
|
||||
"__firefox__scripts": ["assets/js/background.bundle.js"],
|
||||
"__firefox__type": "module"
|
||||
},
|
||||
"__chrome__minimum_chrome_version": "49",
|
||||
"__opera__minimum_opera_version": "36",
|
||||
"__chrome|opera__permissions": [
|
||||
"__chrome__minimum_chrome_version": "88",
|
||||
"__opera__minimum_opera_version": "74",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"clipboardRead",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"__firefox__permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"clipboardWrite",
|
||||
"clipboardRead",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||
"__chrome|opera__options_page": "options.html",
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self'; object-src 'self';"
|
||||
},
|
||||
"__chrome|opera__options_page": "Options/options.html",
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true,
|
||||
"__chrome__chrome_style": false,
|
||||
"__firefox|opera__browser_style": false
|
||||
"page": "Options/options.html",
|
||||
"open_in_tab": true
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 984 B After Width: | Height: | Size: 984 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@@ -1,3 +1,5 @@
|
||||
@use 'variables' as *;
|
||||
|
||||
.icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
@@ -14,32 +16,10 @@
|
||||
border-radius: 100%;
|
||||
|
||||
svg {
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
transition: all 0.2s ease-out 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.max-w-min {
|
||||
max-width: min-content;
|
||||
}
|
||||
|
||||
.max-w-max {
|
||||
max-width: max-content;
|
||||
}
|
||||
|
||||
.max-h-min {
|
||||
max-height: min-content;
|
||||
}
|
||||
|
||||
.max-h-max {
|
||||
max-height: max-content;
|
||||
}
|
||||
|
||||
.h-min {
|
||||
height: min-content;
|
||||
}
|
||||
|
||||
.h-max {
|
||||
height: max-content;
|
||||
.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
67
source/styles/_reset.scss
Normal file
@@ -0,0 +1,67 @@
|
||||
// CSS reset
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: auto;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
img,
|
||||
picture,
|
||||
video,
|
||||
canvas,
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
// Spinner animation (used by Icon component)
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
48
source/styles/_variables.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
// Colors
|
||||
$black: #111111;
|
||||
$light-black: #0f0f0f;
|
||||
$grey-white: #f3f3f3;
|
||||
$white: #ffffff;
|
||||
|
||||
// Tailwind-like colors
|
||||
$gray-200: #e5e7eb;
|
||||
$gray-300: #d1d5db;
|
||||
$gray-400: #9ca3af;
|
||||
$gray-500: #6b7280;
|
||||
$gray-600: #4b5563;
|
||||
$gray-700: #374151;
|
||||
$gray-800: #1f2937;
|
||||
$gray-900: #111827;
|
||||
$red-500: #ef4444;
|
||||
$green-500: #22c55e;
|
||||
$green-900: #14532d;
|
||||
$blue-500: #3b82f6;
|
||||
$indigo-400: #818cf8;
|
||||
$purple-600: #9333ea;
|
||||
$yellow-500: #eab308;
|
||||
|
||||
// Custom colors
|
||||
$copy-icon-bg: hsl(144, 100%, 96%);
|
||||
$stats-total-underline: hsl(200, 35%, 65%);
|
||||
$primary-gradient: linear-gradient(to right, rgb(126, 87, 194), rgb(98, 0, 234));
|
||||
|
||||
// Font weights
|
||||
$thin: 100;
|
||||
$exlight: 200;
|
||||
$light: 300;
|
||||
$regular: 400;
|
||||
$medium: 500;
|
||||
$semibold: 600;
|
||||
$bold: 700;
|
||||
$exbold: 800;
|
||||
$exblack: 900;
|
||||
|
||||
// Transitions
|
||||
$transition-fast: 200ms ease-out;
|
||||
$transition-normal: 300ms ease-in-out;
|
||||
|
||||
// Border radius
|
||||
$radius-sm: 4px;
|
||||
$radius-md: 8px;
|
||||
$radius-lg: 12px;
|
||||
$radius-full: 9999px;
|
||||
@@ -1,17 +0,0 @@
|
||||
@import '~advanced-css-reset/dist/reset.css';
|
||||
|
||||
// Add your custom reset rules here
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// **** colors ****
|
||||
$black: #111111;
|
||||
$light-black: #0f0f0f;
|
||||
$grey-white: #f3f3f3;
|
||||
$white: #ffffff;
|
||||
|
||||
// **** fonts ****
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
$copyIconBg: hsl(144, 100%, 96%);
|
||||
$statsTotalUnderline: hsl(200, 35%, 65%);
|
||||
@@ -1,6 +1,3 @@
|
||||
@import '~tailwindcss/dist/base.min.css';
|
||||
|
||||
@import "base/fonts";
|
||||
@import "base/variables";
|
||||
@import "base/components";
|
||||
@import "base/reset";
|
||||
@use 'reset';
|
||||
@use 'variables';
|
||||
@use 'components';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {browser} from 'webextension-polyfill-ts';
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
const messageUtil = {
|
||||
send(name: string, params?: unknown): Promise<any> {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {browser} from 'webextension-polyfill-ts';
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
import {DomainEntryProperties} from '../Background';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {browser, Tabs} from 'webextension-polyfill-ts';
|
||||
import browser from 'webextension-polyfill';
|
||||
import type {Tabs} from 'webextension-polyfill';
|
||||
|
||||
export function openExtOptionsPage(): Promise<void> {
|
||||
return browser.runtime.openOptionsPage();
|
||||
@@ -7,7 +8,7 @@ export function openExtOptionsPage(): Promise<void> {
|
||||
export function openHistoryPage(): Promise<Tabs.Tab> {
|
||||
return browser.tabs.create({
|
||||
active: true,
|
||||
url: 'history.html',
|
||||
url: 'History/history.html',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
purge: [],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
// Latest stable ECMAScript features
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"useBuiltIns": false,
|
||||
// Do not transform modules to CJS
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"chrome": "49",
|
||||
"firefox": "52",
|
||||
"opera": "36",
|
||||
"edge": "79"
|
||||
}
|
||||
}
|
||||
],
|
||||
"@babel/typescript",
|
||||
"@babel/react"
|
||||
],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-class-properties"],
|
||||
["@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
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
extension/
|
||||
.yarn/
|
||||
.pnp.js
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"@abhijithvijayan/eslint-config/typescript",
|
||||
"@abhijithvijayan/eslint-config/node",
|
||||
"@abhijithvijayan/eslint-config/react"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"./tsconfig.json"
|
||||
],
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"no-extend-native": "off",
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"jsx-a11y/label-has-associated-control": "off",
|
||||
"class-methods-use-this": "off",
|
||||
"max-classes-per-file": "off",
|
||||
"node/no-missing-import": "off",
|
||||
"node/no-unpublished-import": "off",
|
||||
"node/no-unsupported-features/es-syntax": ["error", {
|
||||
"ignores": ["modules"]
|
||||
}]
|
||||
},
|
||||
"env": {
|
||||
"webextensions": true
|
||||
},
|
||||
"settings": {
|
||||
"node": {
|
||||
"tryExtensions": [".tsx"] // append tsx to the list as well
|
||||
}
|
||||
}
|
||||
}
|
||||
12
template/.github/FUNDING.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [abhijithvijayan]
|
||||
patreon: abhijithvijayan
|
||||
open_collective: abhijithvijayan
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: abhijithvijayan
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['https://www.buymeacoffee.com/abhijithvijayan', 'https://www.paypal.me/iamabhijithvijayan']
|
||||
204
template/.gitignore
vendored
@@ -1,204 +0,0 @@
|
||||
# ignore haters
|
||||
haters/
|
||||
|
||||
### WebStorm+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### WebStorm+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/webstorm+all
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# react / gatsby
|
||||
public/
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
### Sass ###
|
||||
.sass-cache/
|
||||
*.css.map
|
||||
*.sass.map
|
||||
*.scss.map
|
||||
|
||||
## Build directory
|
||||
extension/
|
||||
dist/
|
||||
.awcache
|
||||
|
||||
# yarn 2
|
||||
# https://github.com/yarnpkg/berry/issues/454#issuecomment-530312089
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
.pnp.*
|
||||
@@ -1 +0,0 @@
|
||||
v18.18.0
|
||||
@@ -1,19 +0,0 @@
|
||||
language: node_js
|
||||
cache:
|
||||
directories:
|
||||
- ~/.npm
|
||||
node_js:
|
||||
- 12
|
||||
git:
|
||||
depth: 3
|
||||
script:
|
||||
- yarn run build
|
||||
deploy:
|
||||
provider: pages
|
||||
skip-cleanup: true
|
||||
keep-history: true
|
||||
github-token: $GITHUB_TOKEN
|
||||
local-dir: extension
|
||||
target-branch: extension
|
||||
on:
|
||||
branch: master
|
||||
@@ -1,76 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at 34790378+abhijithvijayan@users.noreply.github.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Abhijith Vijayan <email@abhijithvijayan.in> (https://abhijithvijayan.in)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,183 +0,0 @@
|
||||
<h1 align="center">🚀 web-extension-starter</h1>
|
||||
<p align="center">Web Extension starter to build "Write Once Run on Any Browser" extension</p>
|
||||
<p align="center">Update: Rewrite in progress to support manifest v3 / Vite.js: https://github.com/abhijithvijayan/web-extension-starter/tree/vite-rewrite</p>
|
||||
<div align="center">
|
||||
<a href="https://david-dm.org/abhijithvijayan/web-extension-starter">
|
||||
<img src="https://img.shields.io/david/abhijithvijayan/web-extension-starter.svg?colorB=orange" alt="DEPENDENCIES" />
|
||||
</a>
|
||||
<a href="https://github.com/abhijithvijayan/web-extension-starter/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/abhijithvijayan/web-extension-starter.svg" alt="LICENSE" />
|
||||
</a>
|
||||
<a href="https://twitter.com/intent/tweet?text=Check%20out%20web-extension-starter%21%20by%20%40_abhijithv%0A%0AWeb%20Extension%20starter%20to%20build%20%22Write%20Once%20Run%20on%20Any%20Browser%22%20extension.%20https%3A%2F%2Fgithub.com%2Fabhijithvijayan%2Fweb-extension-starter%0A%0A%23javascript%20%23react%20%23typescript%20%23sass%20%23webextension%20%23chrome%20%23firefox%20%23opera">
|
||||
<img src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social" alt="TWEET" />
|
||||
</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 />
|
||||
|
||||
Update: Rewrite to use Vite + React in progress in https://github.com/abhijithvijayan/web-extension-starter/tree/vite-rewrite branch
|
||||
|
||||
❤️ it? ⭐️ it on [GitHub](https://github.com/abhijithvijayan/web-extension-starter) or [Tweet](https://twitter.com/intent/tweet?text=Check%20out%20web-extension-starter%21%20by%20%40_abhijithv%0A%0AWeb%20Extension%20starter%20to%20build%20%22Write%20Once%20Run%20on%20Any%20Browser%22%20extension.%20https%3A%2F%2Fgithub.com%2Fabhijithvijayan%2Fweb-extension-starter%0A%0A%23javascript%20%23react%20%23typescript%20%23sass%20%23webextension%20%23chrome%20%23firefox%20%23opera) about it.
|
||||
|
||||
🧙♂️ **React + TypeScript** = [This](https://github.com/abhijithvijayan/web-extension-starter/tree/react-typescript) branch
|
||||
|
||||
😨 **React + JavaScript** = Checkout [react-javascript](https://github.com/abhijithvijayan/web-extension-starter/tree/react-javascript) branch
|
||||
|
||||
👶🏼 **HTML + JavaScript** = Checkout [master](https://github.com/abhijithvijayan/web-extension-starter/tree/master) branch
|
||||
|
||||
## Features
|
||||
|
||||
- Cross Browser Support (Web-Extensions API)
|
||||
- Browser Tailored Manifest generation
|
||||
- Automatic build on code changes
|
||||
- Auto packs browser specific build files
|
||||
- SASS styling
|
||||
- TypeScript by default
|
||||
- ES6 modules support
|
||||
- React UI Library by default
|
||||
- Smart reload
|
||||
|
||||
## Browser Support
|
||||
|
||||
| [](/) | [](/) | [](/) | [](/) | [](/) | [](/) | [](/) |
|
||||
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| 49 & later ✔ | 52 & later ✔ | 36 & later ✔ | 79 & later ✔ | Latest ✔ | Latest ✔ | Latest ✔ |
|
||||
|
||||
## Used by extensions in production that has over 100,000+ users.
|
||||
|
||||
- [daily.dev](https://daily.dev) in [daily.dev extension](https://r.daily.dev/get)
|
||||
- [Jiffy Reader](https://chrome.google.com/webstore/detail/jiffy-reader/lljedihjnnjjefafchaljkhbpfhfkdic) in [ansh/jiffyreader.com](https://github.com/ansh/jiffyreader.com)
|
||||
- [kutt-extension](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) in [abhijithvijayan/kutt-extension](https://github.com/abhijithvijayan/kutt-extension)
|
||||
- [doubanIMDb](https://chrome.google.com/webstore/detail/doubanimdb/nfibbjnhkbjlgjaojglmmibdjicidini) in [lisongx/doubanIMDb](https://github.com/lisongx/doubanIMDb)
|
||||
- [Mooc Assistant](https://chrome.google.com/webstore/detail/mooc-assistant/oebggekgendmoeedkkdkdcdbmfbfeldc) in [unbyte/mooc-assistant](https://github.com/unbyte/mooc-assistant)
|
||||
- ArtiPub in [crawlab-team/artipub](https://github.com/crawlab-team/artipub/tree/master/extensions)
|
||||
|
||||
and many more...
|
||||
|
||||
## Use this template
|
||||
|
||||
Create a new directory and run
|
||||
|
||||
```
|
||||
curl -fsSL https://github.com/abhijithvijayan/web-extension-starter/archive/react-typescript.tar.gz | tar -xz --strip-components=1
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
Ensure you have
|
||||
|
||||
- [Node.js](https://nodejs.org) 10 or later installed
|
||||
|
||||
Then run the following:
|
||||
|
||||
- `npm install` to install dependencies.
|
||||
- `npm run dev:chrome` to start the development server for chrome extension
|
||||
- `npm run dev:firefox` to start the development server for firefox addon
|
||||
- `npm run dev:opera` to start the development server for opera extension
|
||||
- `npm run build:chrome` to build chrome extension
|
||||
- `npm run build:firefox` to build firefox addon
|
||||
- `npm run build:opera` to build opera extension
|
||||
- `npm run build` builds and packs extensions all at once to extension/ directory
|
||||
|
||||
### Development
|
||||
|
||||
- `npm install` to install dependencies.
|
||||
- To watch file changes in development
|
||||
|
||||
- Chrome
|
||||
- `npm run dev:chrome`
|
||||
- Firefox
|
||||
- `npm run dev:firefox`
|
||||
- Opera
|
||||
- `npm run dev:opera`
|
||||
|
||||
- **Load extension in browser**
|
||||
|
||||
- ### Chrome
|
||||
|
||||
- Go to the browser address bar and type `chrome://extensions`
|
||||
- Check the `Developer Mode` button to enable it.
|
||||
- Click on the `Load Unpacked Extension…` button.
|
||||
- Select your browsers folder in `extension/`.
|
||||
|
||||
- ### Firefox
|
||||
|
||||
- Load the Add-on via `about:debugging` as temporary Add-on.
|
||||
- Choose the `manifest.json` file in the extracted directory
|
||||
|
||||
- ### Opera
|
||||
|
||||
- Load the extension via `opera:extensions`
|
||||
- Check the `Developer Mode` and load as unpacked from extension’s extracted directory.
|
||||
|
||||
### Production
|
||||
|
||||
- `npm run build` builds the extension for all the browsers to `extension/BROWSER` directory respectively.
|
||||
|
||||
Note: By default the `manifest.json` is set with version `0.0.0`. The webpack loader will update the version in the build with that of the `package.json` version. In order to release a new version, update version in `package.json` and run script.
|
||||
|
||||
If you don't want to use `package.json` version, you can disable the option [here](https://github.com/abhijithvijayan/web-extension-starter/blob/e10158c4a49948dea9fdca06592876d9ca04e028/webpack.config.js#L79).
|
||||
|
||||
### Generating browser specific manifest.json
|
||||
|
||||
Update `source/manifest.json` file with browser vendor prefixed manifest keys
|
||||
|
||||
```js
|
||||
{
|
||||
"__chrome__name": "SuperChrome",
|
||||
"__firefox__name": "SuperFox",
|
||||
"__edge__name": "SuperEdge",
|
||||
"__opera__name": "SuperOpera"
|
||||
}
|
||||
```
|
||||
|
||||
if the vendor is `chrome` this compiles to:
|
||||
|
||||
```js
|
||||
{
|
||||
"name": "SuperChrome",
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Add keys to multiple vendors by separating them with | in the prefix
|
||||
|
||||
```
|
||||
{
|
||||
__chrome|opera__name: "SuperBlink"
|
||||
}
|
||||
```
|
||||
|
||||
if the vendor is `chrome` or `opera`, this compiles to:
|
||||
|
||||
```
|
||||
{
|
||||
"name": "SuperBlink"
|
||||
}
|
||||
```
|
||||
|
||||
See the original [README](https://github.com/abhijithvijayan/wext-manifest-loader) of `wext-manifest-loader` package for more details
|
||||
|
||||
## Bugs
|
||||
|
||||
Please file an issue [here](https://github.com/abhijithvijayan/web-extension-starter/issues/new) for bugs, missing documentation, or unexpected behavior.
|
||||
|
||||
### Linting & TypeScript Config
|
||||
|
||||
- Shared Eslint & Prettier Configuration - [`@abhijithvijayan/eslint-config`](https://www.npmjs.com/package/@abhijithvijayan/eslint-config)
|
||||
- Shared TypeScript Configuration - [`@abhijithvijayan/tsconfig`](https://www.npmjs.com/package/@abhijithvijayan/tsconfig)
|
||||
|
||||
## License
|
||||
|
||||
MIT © [Abhijith Vijayan](https://abhijithvijayan.in)
|
||||
11105
template/package-lock.json
generated
@@ -1,87 +0,0 @@
|
||||
{
|
||||
"name": "web-extension-starter",
|
||||
"version": "2.0.0",
|
||||
"description": "Web extension starter using react and typescript",
|
||||
"private": true,
|
||||
"repository": "https://github.com/abhijithvijayan/web-extension-starter.git",
|
||||
"author": {
|
||||
"name": "abhijithvijayan",
|
||||
"email": "email@abhijithvijayan.in",
|
||||
"url": "https://abhijithvijayan.in"
|
||||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0",
|
||||
"yarn": ">= 1.0.0"
|
||||
},
|
||||
"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,.tsx",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.9",
|
||||
"advanced-css-reset": "^1.2.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"webext-base-css": "^1.4.4",
|
||||
"webextension-polyfill-ts": "^0.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@abhijithvijayan/eslint-config": "^2.8.1",
|
||||
"@abhijithvijayan/eslint-config-airbnb": "^1.1.0",
|
||||
"@abhijithvijayan/tsconfig": "^1.3.0",
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/eslint-parser": "^7.23.9",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
|
||||
"@babel/plugin-transform-destructuring": "^7.23.3",
|
||||
"@babel/plugin-transform-runtime": "^7.23.9",
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@types/react": "^17.0.75",
|
||||
"@types/react-dom": "^17.0.25",
|
||||
"@types/webpack": "^5.28.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"babel-loader": "^9.1.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.10.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"filemanager-webpack-plugin": "^8.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^9.0.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"mini-css-extract-plugin": "^2.7.7",
|
||||
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss-loader": "^8.1.0",
|
||||
"prettier": "^3.2.4",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"sass-loader": "^14.1.0",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"typescript": "4.9.5",
|
||||
"webpack": "^5.90.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-ext-reloader": "^1.1.12",
|
||||
"wext-manifest-loader": "^2.4.1",
|
||||
"wext-manifest-webpack-plugin": "^1.4.0"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import {browser} from 'webextension-polyfill-ts';
|
||||
|
||||
browser.runtime.onInstalled.addListener((): void => {
|
||||
console.log('🦄', 'extension installed');
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
console.log('helloworld from content script');
|
||||
|
||||
export {};
|
||||
@@ -1,34 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
const Options: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<form>
|
||||
<p>
|
||||
<label htmlFor="username">Your Name</label>
|
||||
<br />
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
required
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<label htmlFor="logging">
|
||||
<input type="checkbox" name="logging" /> Show the features enabled
|
||||
on each page in the console
|
||||
</label>
|
||||
|
||||
<p>cool cool cool</p>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Options;
|
||||
@@ -1,6 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import Options from './Options';
|
||||
|
||||
ReactDOM.render(<Options />, document.getElementById('options-root'));
|
||||
@@ -1,10 +0,0 @@
|
||||
@import "../styles/fonts";
|
||||
@import "../styles/reset";
|
||||
@import "../styles/variables";
|
||||
|
||||
@import "~webext-base-css/webext-base.css";
|
||||
|
||||
body {
|
||||
color: $black;
|
||||
background-color: $greyWhite;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {browser, Tabs} from 'webextension-polyfill-ts';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
function openWebPage(url: string): Promise<Tabs.Tab> {
|
||||
return browser.tabs.create({url});
|
||||
}
|
||||
|
||||
const Popup: React.FC = () => {
|
||||
return (
|
||||
<section id="popup">
|
||||
<h2>WEB-EXTENSION-STARTER</h2>
|
||||
<button
|
||||
id="options__button"
|
||||
type="button"
|
||||
onClick={(): Promise<Tabs.Tab> => {
|
||||
return openWebPage('options.html');
|
||||
}}
|
||||
>
|
||||
Options Page
|
||||
</button>
|
||||
<div className="links__holder">
|
||||
<ul>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(): Promise<Tabs.Tab> => {
|
||||
return openWebPage(
|
||||
'https://github.com/abhijithvijayan/web-extension-starter'
|
||||
);
|
||||
}}
|
||||
>
|
||||
GitHub
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(): Promise<Tabs.Tab> => {
|
||||
return openWebPage(
|
||||
'https://www.buymeacoffee.com/abhijithvijayan'
|
||||
);
|
||||
}}
|
||||
>
|
||||
Buy Me A Coffee
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Popup;
|
||||
@@ -1,6 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import Popup from './Popup';
|
||||
|
||||
ReactDOM.render(<Popup />, document.getElementById('popup-root'));
|
||||
@@ -1,53 +0,0 @@
|
||||
@import "../styles/fonts";
|
||||
@import "../styles/reset";
|
||||
@import "../styles/variables";
|
||||
|
||||
body {
|
||||
color: $black;
|
||||
background-color: $greyWhite;
|
||||
}
|
||||
|
||||
|
||||
#popup {
|
||||
min-width: 350px;
|
||||
padding: 30px 20px;
|
||||
|
||||
h2 {
|
||||
font-size: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#options__button {
|
||||
width: 50%;
|
||||
background: green;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
border-radius: 15px;
|
||||
padding: 5px 10px;
|
||||
justify-content: center;
|
||||
margin: 20px auto;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.links__holder {
|
||||
ul {
|
||||
display: flex;
|
||||
margin-top: 1em;
|
||||
justify-content: space-around;
|
||||
|
||||
li {
|
||||
button {
|
||||
border-radius: 25px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
padding: 10px 17px;
|
||||
background-color: rgba(0, 0, 255, 0.7);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,75 +0,0 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Sample WebExtension",
|
||||
"version": "0.0.0",
|
||||
|
||||
"icons": {
|
||||
"16": "assets/icons/favicon-16.png",
|
||||
"32": "assets/icons/favicon-32.png",
|
||||
"48": "assets/icons/favicon-48.png",
|
||||
"128": "assets/icons/favicon-128.png"
|
||||
},
|
||||
"description": "Sample description",
|
||||
"homepage_url": "https://github.com/abhijithvijayan/web-extension-starter",
|
||||
"short_name": "Sample Name",
|
||||
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
||||
"__chrome|firefox__author": "abhijithvijayan",
|
||||
"__opera__developer": {
|
||||
"name": "abhijithvijayan"
|
||||
},
|
||||
|
||||
"__firefox__applications": {
|
||||
"gecko": {
|
||||
"id": "{754FB1AD-CC3B-4856-B6A0-7786F8CA9D17}"
|
||||
}
|
||||
},
|
||||
|
||||
"__chrome__minimum_chrome_version": "49",
|
||||
"__opera__minimum_opera_version": "36",
|
||||
|
||||
"browser_action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "assets/icons/favicon-16.png",
|
||||
"32": "assets/icons/favicon-32.png",
|
||||
"48": "assets/icons/favicon-48.png",
|
||||
"128": "assets/icons/favicon-128.png"
|
||||
},
|
||||
"default_title": "tiny title",
|
||||
"__chrome|opera__chrome_style": false,
|
||||
"__firefox__browser_style": false
|
||||
},
|
||||
|
||||
"__chrome|opera__options_page": "options.html",
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true,
|
||||
"__chrome__chrome_style": false
|
||||
},
|
||||
|
||||
"background": {
|
||||
"scripts": [
|
||||
"js/background.bundle.js"
|
||||
],
|
||||
"__chrome|opera__persistent": false
|
||||
},
|
||||
|
||||
"content_scripts": [{
|
||||
"matches": [
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"js": [
|
||||
"js/contentScript.bundle.js"
|
||||
]
|
||||
}]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito:400,600");
|
||||
@@ -1,10 +0,0 @@
|
||||
@import '~advanced-css-reset/dist/reset.css';
|
||||
|
||||
// Add your custom reset rules here
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// colors
|
||||
$black: #0d0d0d;
|
||||
$greyWhite: #f3f3f3;
|
||||
$skyBlue: #8892b0;
|
||||
|
||||
// fonts
|
||||
$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;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"extends": "@abhijithvijayan/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "es5", // ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'.
|
||||
"module": "esnext", // Module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"declaration": false,
|
||||
"isolatedModules": true,
|
||||
/* Additional Checks */
|
||||
"useDefineForClassFields": true,
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"include": [
|
||||
"source",
|
||||
"webpack.config.js"
|
||||
]
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const FilemanagerPlugin = require('filemanager-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
|
||||
const ExtensionReloader = require('webpack-ext-reloader');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const WextManifestWebpackPlugin = require('wext-manifest-webpack-plugin');
|
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
||||
const CSSMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||
|
||||
const viewsPath = path.join(__dirname, 'views');
|
||||
const sourcePath = path.join(__dirname, 'source');
|
||||
const destPath = path.join(__dirname, 'extension');
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const targetBrowser = process.env.TARGET_BROWSER;
|
||||
|
||||
const extensionReloaderPlugin =
|
||||
nodeEnv === 'development'
|
||||
? new ExtensionReloader({
|
||||
port: 9090, // Which port use to create the server
|
||||
reloadPage: true, // Force the reload of the page also
|
||||
entries: {
|
||||
// TODO: reload manifest on update
|
||||
contentScript: 'contentScript',
|
||||
background: 'background',
|
||||
extensionPage: ['popup', 'options'],
|
||||
},
|
||||
})
|
||||
: () => {
|
||||
this.apply = () => {};
|
||||
};
|
||||
|
||||
const getExtensionFileType = (browser) => {
|
||||
if (browser === 'opera') {
|
||||
return 'crx';
|
||||
}
|
||||
|
||||
if (browser === 'firefox') {
|
||||
return 'xpi';
|
||||
}
|
||||
|
||||
return 'zip';
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
devtool: false, // https://github.com/webpack/webpack/issues/1194#issuecomment-560382342
|
||||
|
||||
stats: {
|
||||
all: false,
|
||||
builtAt: true,
|
||||
errors: true,
|
||||
hash: true,
|
||||
},
|
||||
|
||||
mode: nodeEnv,
|
||||
|
||||
entry: {
|
||||
manifest: path.join(sourcePath, 'manifest.json'),
|
||||
background: path.join(sourcePath, 'Background', 'index.ts'),
|
||||
contentScript: path.join(sourcePath, 'ContentScript', 'index.ts'),
|
||||
popup: path.join(sourcePath, 'Popup', 'index.tsx'),
|
||||
options: path.join(sourcePath, 'Options', 'index.tsx'),
|
||||
},
|
||||
|
||||
output: {
|
||||
path: path.join(destPath, targetBrowser),
|
||||
filename: 'js/[name].bundle.js',
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||
alias: {
|
||||
'webextension-polyfill-ts': path.resolve(
|
||||
path.join(__dirname, 'node_modules', 'webextension-polyfill-ts')
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
type: 'javascript/auto', // prevent webpack handling json with its own loaders,
|
||||
test: /manifest\.json$/,
|
||||
use: {
|
||||
loader: 'wext-manifest-loader',
|
||||
options: {
|
||||
usePackageJSONVersion: true, // set to false to not use package.json version for manifest
|
||||
},
|
||||
},
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.(js|ts)x?$/,
|
||||
loader: 'babel-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: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
postcssOptions: {
|
||||
plugins: [
|
||||
[
|
||||
'autoprefixer',
|
||||
{
|
||||
// Options
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'resolve-url-loader', // Rewrites relative paths in url() statements
|
||||
'sass-loader', // Takes the Sass/SCSS file and compiles to the CSS
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
// Plugin to not generate js bundle for manifest entry
|
||||
new WextManifestWebpackPlugin(),
|
||||
// Generate sourcemaps
|
||||
new webpack.SourceMapDevToolPlugin({filename: false}),
|
||||
new ForkTsCheckerWebpackPlugin(),
|
||||
// environmental 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(targetBrowser)}`
|
||||
),
|
||||
],
|
||||
cleanStaleWebpackAssets: false,
|
||||
verbose: true,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(viewsPath, 'popup.html'),
|
||||
inject: 'body',
|
||||
chunks: ['popup'],
|
||||
hash: true,
|
||||
filename: 'popup.html',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(viewsPath, 'options.html'),
|
||||
inject: 'body',
|
||||
chunks: ['options'],
|
||||
hash: true,
|
||||
filename: 'options.html',
|
||||
}),
|
||||
// write css file(s) to build folder
|
||||
new MiniCssExtractPlugin({filename: 'css/[name].css'}),
|
||||
// copy static assets
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{from: 'source/assets', to: 'assets'}],
|
||||
}),
|
||||
// plugin to enable browser reloading in development mode
|
||||
extensionReloaderPlugin,
|
||||
],
|
||||
|
||||
optimization: {
|
||||
minimize: true,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
parallel: true,
|
||||
terserOptions: {
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
},
|
||||
extractComments: false,
|
||||
}),
|
||||
new CSSMinimizerPlugin({
|
||||
minimizerOptions: {
|
||||
preset: [
|
||||
"default",
|
||||
{ discardComments: { removeAll: true } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
new FilemanagerPlugin({
|
||||
events: {
|
||||
onEnd: {
|
||||
archive: [
|
||||
{
|
||||
format: 'zip',
|
||||
source: path.join(destPath, targetBrowser),
|
||||
destination: `${path.join(destPath, targetBrowser)}.${getExtensionFileType(targetBrowser)}`,
|
||||
options: {zlib: {level: 6}},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,23 +1,15 @@
|
||||
{
|
||||
"extends": "@abhijithvijayan/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "es5", // ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'.
|
||||
"module": "esnext", // Module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"declaration": false,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"noEmit": true,
|
||||
"lib": ["dom", "dom.iterable", "ES2023"],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
/* Additional Checks */
|
||||
"useDefineForClassFields": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": false,
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"source",
|
||||
"twin.d.ts",
|
||||
"cssprop.d.ts",
|
||||
"webpack.config.js"
|
||||
]
|
||||
"include": ["source"]
|
||||
}
|
||||
7
twin.d.ts
vendored
@@ -1,7 +0,0 @@
|
||||
import 'twin.macro';
|
||||
import styledComponent, {css as cssProperty} from 'styled-components';
|
||||
|
||||
declare module 'twin.macro' {
|
||||
const css: typeof cssProperty;
|
||||
const styled: typeof styledComponent;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>History: Kutt</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="history-root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||