feat: migrate to vite and manifest v3

This commit is contained in:
abhijithvijayan
2026-01-03 23:17:21 +05:30
parent 021db2b5fc
commit a962d6b2b3
105 changed files with 9685 additions and 23034 deletions

View File

@@ -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
View 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

2
.nvmrc
View File

@@ -1 +1 @@
12 20

View File

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

View File

@@ -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> <h1 align="center">kutt-extension</h1>
<p align="center">Browser extension for <a href="https://kutt.it">Kutt.it</a></p> <p align="center">Browser extension for <a href="https://kutt.it">Kutt.it</a></p>
<div align="center"> <div align="center">
<a href="https://travis-ci.com/thedevs-network/kutt-extension"> <a href="https://github.com/thedevs-network/kutt-extension/actions/workflows/build.yml">
<img src="https://travis-ci.com/thedevs-network/kutt-extension.svg?branch=master" alt="Travis Build" /> <img src="https://github.com/thedevs-network/kutt-extension/actions/workflows/build.yml/badge.svg?branch=master" alt="Build" />
</a> </a>
<a href="https://github.com/thedevs-network/kutt-extension/releases/latest"> <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" /> <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"> <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" /> <img src="https://img.shields.io/github/issues-closed-raw/thedevs-network/kutt-extension.svg?colorB=red" alt="Closed Issues" />
</a> </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"> <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" /> <img src="https://img.shields.io/github/license/thedevs-network/kutt-extension.svg" alt="LICENSE" />
</a> </a>

View File

@@ -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
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -10,29 +10,19 @@
"url": "https://abhijithvijayan.in" "url": "https://abhijithvijayan.in"
}, },
"engines": { "engines": {
"node": ">=10.0.0", "node": ">=20"
"yarn": ">=1.0.0"
}, },
"type": "module",
"scripts": { "scripts": {
"dev:chrome": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome webpack --watch", "dev:chrome": "cross-env TARGET_BROWSER=chrome vite build --config vite.config.ts --mode development --watch",
"dev:firefox": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=firefox webpack --watch", "dev:firefox": "cross-env TARGET_BROWSER=firefox vite build --config vite.config.ts --mode development --watch",
"dev:opera": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=opera webpack --watch", "dev:opera": "cross-env TARGET_BROWSER=opera vite build --config vite.config.ts --mode development --watch",
"build:chrome": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome webpack", "build:chrome": "cross-env TARGET_BROWSER=chrome vite build --config vite.config.ts",
"build:firefox": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=firefox webpack", "build:firefox": "cross-env TARGET_BROWSER=firefox vite build --config vite.config.ts",
"build:opera": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=opera webpack", "build:opera": "cross-env TARGET_BROWSER=opera vite build --config vite.config.ts",
"build": "yarn run build:chrome && yarn run build:firefox && yarn run build:opera", "build": "npm run build:chrome && npm run build:firefox && npm run build:opera",
"lint": "eslint . --ext .ts,.tsx", "lint": "eslint .",
"lint:fix": "eslint . --ext .ts,.tsx --fix" "lint:fix": "eslint . --fix"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint . --ext .ts,.tsx"
]
}, },
"keywords": [ "keywords": [
"url", "url",
@@ -45,78 +35,44 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@abhijithvijayan/ts-utils": "^1.2.2", "@abhijithvijayan/ts-utils": "^1.2.2",
"@babel/runtime": "^7.14.6", "advanced-css-reset": "^2.1.3",
"advanced-css-reset": "^1.2.2", "axios": "^1.7.9",
"axios": "^0.21.1", "clsx": "^2.1.1",
"qrcode.react": "^1.0.1", "qrcode.react": "^4.2.0",
"react": "^17.0.2", "react": "^19.0.0",
"react-copy-to-clipboard": "^5.0.3", "react-copy-to-clipboard": "^5.1.0",
"react-dom": "^17.0.2", "react-dom": "^19.0.0",
"react-use-form-state": "^0.13.2", "webextension-polyfill": "^0.12.0"
"styled-components": "^5.3.0",
"twin.macro": "^1.12.1",
"webextension-polyfill-ts": "^0.26.0"
}, },
"devDependencies": { "devDependencies": {
"@abhijithvijayan/eslint-config": "2.6.3", "@abhijithvijayan/eslint-config": "^3.0.0",
"@abhijithvijayan/eslint-config-airbnb": "^1.0.2", "@abhijithvijayan/tsconfig": "^1.5.1",
"@abhijithvijayan/tsconfig": "^1.3.0", "@types/node": "^22.10.2",
"@babel/core": "^7.14.6", "@types/react": "^19.0.2",
"@babel/eslint-parser": "^7.14.7", "@types/react-copy-to-clipboard": "^5.0.7",
"@babel/plugin-proposal-class-properties": "^7.14.5", "@types/react-dom": "^19.0.2",
"@babel/plugin-proposal-object-rest-spread": "^7.14.7", "@types/webextension-polyfill": "^0.12.1",
"@babel/plugin-transform-destructuring": "^7.14.7", "@typescript-eslint/eslint-plugin": "^8.18.1",
"@babel/plugin-transform-react-jsx": "^7.14.5", "@typescript-eslint/parser": "^8.18.1",
"@babel/plugin-transform-runtime": "^7.14.5", "@vitejs/plugin-react": "^4.3.4",
"@babel/preset-env": "^7.14.7", "autoprefixer": "^10.4.20",
"@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",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^5.2.7", "eslint": "^9.17.0",
"eslint": "^7.30.0", "eslint-config-prettier": "^9.1.0",
"eslint-config-prettier": "^6.15.0", "eslint-plugin-import-x": "^4.5.0",
"eslint-plugin-import": "^2.23.4", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-n": "^17.15.1",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-react": "^7.37.2",
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-hooks": "^4.2.0", "globals": "^15.14.0",
"filemanager-webpack-plugin": "^3.1.1", "postcss": "^8.4.49",
"fork-ts-checker-webpack-plugin": "^6.2.12", "prettier": "^3.4.2",
"html-webpack-plugin": "^4.5.2", "sass": "^1.83.0",
"husky": "^6.0.0", "typescript": "^5.7.2",
"lint-staged": "^11.0.1", "vite": "^6.0.5",
"mini-css-extract-plugin": "^1.6.2", "vite-plugin-checker": "^0.8.0",
"node-sass": "^4.14.1", "vite-plugin-wext-manifest": "^1.2.2",
"optimize-css-assets-webpack-plugin": "^5.0.8", "vite-plugin-zip-pack": "^1.2.4"
"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"
} }
} }

5
postcss.config.js Normal file
View File

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

View File

@@ -6,9 +6,8 @@
*/ */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import {browser} from 'webextension-polyfill-ts'; import browser, {Runtime} from 'webextension-polyfill';
import axios, {AxiosPromise, AxiosError} from 'axios';
import axios, {AxiosPromise} from 'axios';
import * as constants from './constants'; import * as constants from './constants';
export enum Kutt { export enum Kutt {
@@ -113,7 +112,8 @@ async function shortenUrl({
error: false, error: false,
data, data,
}; };
} catch (err) { } catch (error) {
const err = error as AxiosError<{error?: string}>;
if (err.response) { if (err.response) {
if (err.response.status === 401) { if (err.response.status === 401) {
return { return {
@@ -129,7 +129,7 @@ async function shortenUrl({
) { ) {
return { return {
error: true, error: true,
message: `Error: ${err.response.data.error}`, message: `Error: ${err.response.data?.error}`,
}; };
} }
@@ -201,7 +201,8 @@ async function checkApiKey({
error: false, error: false,
data, data,
}; };
} catch (err) { } catch (error) {
const err = error as AxiosError;
if (err.response) { if (err.response) {
if (err.response.status === 401) { if (err.response.status === 401) {
return { return {
@@ -283,7 +284,8 @@ async function fetchUrlsHistory({
error: false, error: false,
data, data,
}; };
} catch (err) { } catch (error) {
const err = error as AxiosError;
if (err.response) { if (err.response) {
if (err.response.status === 401) { if (err.response.status === 401) {
return { 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 * Listen for messages from UI pages
*/ */
browser.runtime.onMessage.addListener( 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 consistent-return
// eslint-disable-next-line default-case // eslint-disable-next-line default-case
switch (request.action) { switch (request.action) {

View 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;
}

View File

@@ -1,5 +1,4 @@
import React, {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import 'twin.macro';
import { import {
useShortenedLinks, useShortenedLinks,
@@ -30,7 +29,9 @@ import Loader from '../components/Loader';
import Header from '../Options/Header'; import Header from '../Options/Header';
import Table from './Table'; import Table from './Table';
const History: React.FC = () => { import styles from './History.module.scss';
function History() {
const [, shortenedLinksDispatch] = useShortenedLinks(); const [, shortenedLinksDispatch] = useShortenedLinks();
const [, extensionSettingsDispatch] = useExtensionSettings(); const [, extensionSettingsDispatch] = useExtensionSettings();
const [requestStatusState, requestStatusDispatch] = useRequestStatus(); const [requestStatusState, requestStatusDispatch] = useRequestStatus();
@@ -56,7 +57,7 @@ const History: React.FC = () => {
.replace('http://', '') .replace('http://', '')
.replace('https://', '') .replace('https://', '')
.replace('www.', '') .replace('www.', '')
.split(/[/?#]/)[0], // extract domain .split(/[/?#]/)[0] || '', // extract domain
hostUrl: (settings.host as string).endsWith('/') hostUrl: (settings.host as string).endsWith('/')
? (settings.host as string).slice(0, -1) ? (settings.host as string).slice(0, -1)
: (settings.host as string), // slice `/` at the end : (settings.host as string), // slice `/` at the end
@@ -129,8 +130,8 @@ const History: React.FC = () => {
return ( return (
<BodyWrapper> <BodyWrapper>
<div id="history" tw="h-screen bg-gray-200"> <div id="history" className={styles.historyPage}>
<div tw={'overflow-x-hidden px-6 py-8'}> <div className={styles.historyContent}>
<Header /> <Header />
{/* eslint-disable-next-line no-nested-ternary */} {/* eslint-disable-next-line no-nested-ternary */}
@@ -138,10 +139,10 @@ const History: React.FC = () => {
!errored.error ? ( !errored.error ? (
<Table /> <Table />
) : ( ) : (
<h2>{errored.message}</h2> <h2 className={styles.errorMessage}>{errored.message}</h2>
) )
) : ( ) : (
<div tw="h-10"> <div className={styles.loaderContainer}>
<Loader /> <Loader />
</div> </div>
)} )}
@@ -149,6 +150,6 @@ const History: React.FC = () => {
</div> </div>
</BodyWrapper> </BodyWrapper>
); );
}; }
export default History; export default History;

View 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;
}
}

View File

@@ -1,55 +1,26 @@
import tw, {css} from 'twin.macro'; import {QRCodeSVG} from 'qrcode.react';
import QRCode from 'qrcode.react'; import {Dispatch, SetStateAction} from 'react';
import React from 'react';
import styles from './Modal.module.scss';
type Props = { type Props = {
link: string; 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 ( return (
<> <>
<div <div className={styles.modalOverlay}>
css={[ <div className={styles.modalContent}>
tw`fixed top-0 left-0 flex items-center justify-center w-full h-full`, <div className={styles.qrCodeWrapper}>
<QRCodeSVG size={196} value={link} />
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> </div>
<div tw="flex justify-center mt-10"> <div className={styles.buttonWrapper}>
<button <button
onClick={(): void => setModalView(false)} onClick={(): void => setModalView(false)}
css={[ className={styles.closeButton}
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;
`,
]}
type="button" type="button"
> >
Close Close
@@ -59,6 +30,6 @@ const Modal: React.FC<Props> = ({link, setModalView}) => {
</div> </div>
</> </>
); );
}; }
export default Modal; export default Modal;

View 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;
}

View File

@@ -1,6 +1,6 @@
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import React, {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import tw, {css, styled} from 'twin.macro'; import clsx from 'clsx';
import { import {
useShortenedLinks, useShortenedLinks,
@@ -11,39 +11,16 @@ import {MAX_HISTORY_ITEMS} from '../Background/constants';
import Icon from '../components/Icon'; import Icon from '../components/Icon';
import Modal from './Modal'; import Modal from './Modal';
const StyledTd = styled.td` import styles from './Table.module.scss';
${tw`relative flex items-center px-0 py-4`}
`;
const StyledIcon = styled(Icon)` function Table() {
${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 = () => {
const [shortenedLinksState, shortenedLinksDispatch] = useShortenedLinks(); const [shortenedLinksState, shortenedLinksDispatch] = useShortenedLinks();
const [QRView, setQRView] = useState<boolean>(false); const [QRView, setQRView] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false); const [copied, setCopied] = useState<boolean>(false);
// reset copy message // reset copy message
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout | null = null; let timer: ReturnType<typeof setTimeout> | null = null;
timer = setTimeout(() => { timer = setTimeout(() => {
setCopied(false); setCopied(false);
@@ -77,186 +54,49 @@ const Table: React.FC = () => {
return ( return (
<> <>
<div <div className={styles.tableContainer}>
css={[ <div className={styles.tableWrapper}>
tw` flex flex-col items-center w-full min-h-screen mt-5 mb-3`, <div className={styles.tableHeader}>
<h2 className={styles.tableTitle}>
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`]}>
Recent shortened links. (last {MAX_HISTORY_ITEMS} results) Recent shortened links. (last {MAX_HISTORY_ITEMS} results)
</h2> </h2>
</div> </div>
<table <table className={styles.table}>
css={[ <thead className={styles.thead}>
tw`flex flex-col flex-auto bg-white`, <tr className={styles.theadRow}>
<th className={clsx(styles.th, styles.thOriginal)}>
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;
`,
]}
>
Original URL Original URL
</th> </th>
<th <th className={clsx(styles.th, styles.thShort)}>Short URL</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>
</tr> </tr>
</thead> </thead>
<tbody tw="flex flex-col flex-auto"> <tbody className={styles.tbody}>
{!(shortenedLinksState.total === 0) ? ( {!(shortenedLinksState.total === 0) ? (
shortenedLinksState.items.map((item) => { shortenedLinksState.items.map((item) => {
return ( return (
<tr <tr key={item.id} className={styles.tr}>
key={item.id} <td className={clsx(styles.td, styles.tdOriginal)}>
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
);
}
`,
]}
>
<a <a
css={[ className={styles.link}
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);
`,
]}
href={item.target} href={item.target}
target="_blank" target="_blank"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
> >
{item.target} {item.target}
</a> </a>
</StyledTd> </td>
<StyledTd <td className={clsx(styles.td, styles.tdShort)}>
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
);
}
`,
]}
>
{copied && {copied &&
shortenedLinksState.selected?.id === item.id && ( shortenedLinksState.selected?.id === item.id && (
<div <div className={styles.copiedNotification}>
css={[
tw`absolute top-0 left-0 text-xs text-green-900`,
css`
font-size: 11px;
`,
]}
>
Copied to clipboard! Copied to clipboard!
</div> </div>
)} )}
<div tw="flex items-center"> <div className={styles.shortUrlWrapper}>
<a <a
css={[ className={styles.link}
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);
`,
]}
href={item.link} href={item.link}
target="_blank" target="_blank"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
@@ -264,15 +104,15 @@ const Table: React.FC = () => {
{item.link} {item.link}
</a> </a>
</div> </div>
</StyledTd> </td>
<StyledTd> <td className={styles.td}>
<div tw="flex items-center justify-end"> <div className={styles.actionsWrapper}>
{/* // **** COPY TO CLIPBOARD **** // */} {/* // **** COPY TO CLIPBOARD **** // */}
{copied && {copied &&
shortenedLinksState.selected?.id === item.id ? ( shortenedLinksState.selected?.id === item.id ? (
<StyledIcon name="tick" className="icon" /> <Icon name="tick" className={styles.actionIcon} />
) : ( ) : (
<CopyToClipboard <CopyToClipboard
text={item.link} text={item.link}
@@ -280,15 +120,15 @@ const Table: React.FC = () => {
return handleCopyToClipboard(item.id); return handleCopyToClipboard(item.id);
}} }}
> >
<StyledIcon name="copy" className="icon" /> <Icon name="copy" className={styles.actionIcon} />
</CopyToClipboard> </CopyToClipboard>
)} )}
<StyledIcon <Icon
onClick={(): void => onClick={(): void =>
handleQRCodeViewToggle(item.id) handleQRCodeViewToggle(item.id)
} }
className="icon" className={styles.actionIcon}
name="qrcode" name="qrcode"
/> />
</div> </div>
@@ -298,13 +138,13 @@ const Table: React.FC = () => {
shortenedLinksState.selected?.id === item.id && ( shortenedLinksState.selected?.id === item.id && (
<Modal link={item.link} setModalView={setQRView} /> <Modal link={item.link} setModalView={setQRView} />
)} )}
</StyledTd> </td>
</tr> </tr>
); );
}) })
) : ( ) : (
<tr> <tr>
<td>No URLs History</td> <td className={styles.emptyRow}>No URLs History</td>
</tr> </tr>
)} )}
</tbody> </tbody>
@@ -313,6 +153,6 @@ const Table: React.FC = () => {
</div> </div>
</> </>
); );
}; }
export default Table; export default Table;

View File

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

View File

@@ -1,21 +1,21 @@
import {ThemeProvider} from 'styled-components'; import {StrictMode} from 'react';
import React from 'react'; import {createRoot} from 'react-dom/client';
import ReactDOM from 'react-dom';
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 './styles.scss';
import History from './History'; const container = document.getElementById('history-root');
import {ExtensionSettingsProvider} from '../contexts/extension-settings-context'; if (!container) {
import {ShortenedLinksProvider} from '../contexts/shortened-links-context'; throw new Error('Could not find history-root container');
import {RequestStatusProvider} from '../contexts/request-status-context'; }
// eslint-disable-next-line import/no-webpack-loader-syntax, import/no-unresolved, @typescript-eslint/no-var-requires, node/no-missing-require const root = createRoot(container);
const theme = require('sass-extract-loader?{"plugins": ["sass-extract-js"]}!../styles/base/_variables.scss'); root.render(
// Require sass variables using sass-extract-loader and specify the plugin <StrictMode>
ReactDOM.render(
<ThemeProvider theme={theme}>
<ExtensionSettingsProvider> <ExtensionSettingsProvider>
<RequestStatusProvider> <RequestStatusProvider>
<ShortenedLinksProvider> <ShortenedLinksProvider>
@@ -23,6 +23,5 @@ ReactDOM.render(
</ShortenedLinksProvider> </ShortenedLinksProvider>
</RequestStatusProvider> </RequestStatusProvider>
</ExtensionSettingsProvider> </ExtensionSettingsProvider>
</ThemeProvider>, </StrictMode>
document.getElementById('history-root')
); );

View File

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

View 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;
}

View File

@@ -1,17 +1,19 @@
import React from 'react'; import {memo} from 'react';
import 'twin.macro'; import clsx from 'clsx';
import {detectBrowser} from '../util/browser'; import {detectBrowser} from '../util/browser';
import {StoreLinks} from '../Background'; import {StoreLinks} from '../Background';
import Icon from '../components/Icon'; import Icon from '../components/Icon';
const Footer: React.FC = () => { import styles from './Footer.module.scss';
function Footer() {
return ( return (
<> <>
<footer tw="sm:mt-6 xl:mt-16 lg:mt-10 md:mt-8 py-4 mt-4 font-normal text-xs"> <footer className={styles.footer}>
<div tw="flex items-center text-gray-800"> <div className={styles.ratingSection}>
<span tw="block w-1/3 mr-2 border border-gray-200" /> <span className={clsx(styles.dividerLine, styles.left)} />
<a <a
href={ href={
detectBrowser() === 'firefox' detectBrowser() === 'firefox'
@@ -20,42 +22,44 @@ const Footer: React.FC = () => {
} }
target="_blank" target="_blank"
rel="nofollow noopener noreferrer" rel="nofollow noopener noreferrer"
tw="flex flex-col items-center justify-center" className={styles.ratingLink}
> >
<div tw="flex items-center mt-1"> <div className={styles.starsContainer}>
<Icon tw="mr-1 text-yellow-500 fill-current" name="star-yellow" /> <Icon className={clsx(styles.starIcon, styles.yellow)} name="star-yellow" />
<Icon tw="mr-1 text-yellow-500 fill-current" name="star-yellow" /> <Icon className={clsx(styles.starIcon, styles.yellow)} name="star-yellow" />
<Icon tw="mr-1 text-yellow-500 fill-current" name="star-yellow" /> <Icon className={clsx(styles.starIcon, styles.yellow)} name="star-yellow" />
<Icon tw="mr-1 text-yellow-500 fill-current" name="star-yellow" /> <Icon className={clsx(styles.starIcon, styles.yellow)} name="star-yellow" />
<Icon tw="text-gray-400 fill-current" name="star-white" /> <Icon className={clsx(styles.starIcon, styles.gray)} name="star-white" />
</div> </div>
<p tw="mb-0 mt-1">Rate on Store</p> <p className={styles.ratingText}>Rate on Store</p>
</a> </a>
<span tw="block w-1/3 ml-2 border border-gray-200" /> <span className={clsx(styles.dividerLine, styles.right)} />
</div> </div>
<div tw="flex items-center justify-around text-center divide-x divide-gray-300 my-4"> <div className={styles.linksSection}>
<a <a
href="https://kutt.it" href="https://kutt.it"
target="blank" target="blank"
rel="nofollow noopener noreferrer" rel="nofollow noopener noreferrer"
tw="w-1/3 p-1 cursor-pointer hover:text-gray-800" className={clsx(styles.linkItem, styles.narrow)}
> >
Kutt.it Kutt.it
</a> </a>
<span className={styles.linkDivider} />
<a <a
href={'https://git.io/Jn5hS'} href={'https://git.io/Jn5hS'}
target="blank" target="blank"
rel="nofollow noopener noreferrer" 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 Report an issue
</a> </a>
<span className={styles.linkDivider} />
<a <a
href="https://github.com/thedevs-network/kutt-extension" href="https://github.com/thedevs-network/kutt-extension"
target="blank" target="blank"
rel="nofollow noopener noreferrer" rel="nofollow noopener noreferrer"
tw="w-1/3 p-1 cursor-pointer hover:text-gray-800" className={clsx(styles.linkItem, styles.narrow)}
> >
GitHub GitHub
</a> </a>
@@ -63,6 +67,6 @@ const Footer: React.FC = () => {
</footer> </footer>
</> </>
); );
}; }
export default React.memo(Footer); export default memo(Footer);

View 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;
}
}

View File

@@ -1,7 +1,6 @@
import {isNull, isUndefined} from '@abhijithvijayan/ts-utils'; import {isNull, isUndefined} from '@abhijithvijayan/ts-utils';
import {useFormState} from 'react-use-form-state'; import {useState, useEffect, ChangeEvent} from 'react';
import React, {useState, useEffect} from 'react'; import clsx from 'clsx';
import tw, {styled} from 'twin.macro';
import {useExtensionSettings} from '../contexts/extension-settings-context'; import {useExtensionSettings} from '../contexts/extension-settings-context';
import {updateExtensionSettings} from '../util/settings'; import {updateExtensionSettings} from '../util/settings';
@@ -18,6 +17,8 @@ import {
import Icon from '../components/Icon'; import Icon from '../components/Icon';
import styles from './Form.module.scss';
type OptionsFormValuesProperties = { type OptionsFormValuesProperties = {
apikey: string; apikey: string;
history: boolean; history: boolean;
@@ -25,22 +26,15 @@ type OptionsFormValuesProperties = {
host: string; host: string;
}; };
const StyledValidateButton = styled.button` type FormErrors = {
${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`} apikey?: string;
host?: string;
};
background: linear-gradient(to right,rgb(126, 87, 194),rgb(98, 0, 234)); type FormValidity = {
apikey?: boolean;
.validate__icon { host?: boolean;
${tw`inline-flex px-0 bg-transparent`} };
svg {
${tw`transition-transform duration-300 ease-in-out`}
stroke: currentColor;
stroke-width: 2;
}
}
`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const onSave = (values: OptionsFormValuesProperties): Promise<any> => { const onSave = (values: OptionsFormValuesProperties): Promise<any> => {
@@ -48,7 +42,7 @@ const onSave = (values: OptionsFormValuesProperties): Promise<any> => {
return updateExtensionSettings(values); // update local settings return updateExtensionSettings(values); // update local settings
}; };
const Form: React.FC = () => { function Form() {
const extensionSettingsState = useExtensionSettings()[0]; const extensionSettingsState = useExtensionSettings()[0];
const [submitting, setSubmitting] = useState<boolean>(false); const [submitting, setSubmitting] = useState<boolean>(false);
const [showApiKey, setShowApiKey] = useState<boolean>(false); const [showApiKey, setShowApiKey] = useState<boolean>(false);
@@ -56,21 +50,8 @@ const Form: React.FC = () => {
error: null, error: null,
message: '', message: '',
}); });
const [
formState, const [formValues, setFormValues] = useState<OptionsFormValuesProperties>({
{
text: textProps,
checkbox: checkboxProps,
password: passwordProps,
label: labelProps,
},
] = useFormState<{
apikey: string;
history: boolean;
advanced: boolean;
host: string;
}>(
{
apikey: extensionSettingsState.apikey, apikey: extensionSettingsState.apikey,
history: extensionSettingsState.history, history: extensionSettingsState.history,
advanced: extensionSettingsState.advanced, advanced: extensionSettingsState.advanced,
@@ -78,68 +59,73 @@ const Form: React.FC = () => {
(extensionSettingsState.advanced && (extensionSettingsState.advanced &&
extensionSettingsState.host.hostUrl) || extensionSettingsState.host.hostUrl) ||
'', '',
}, });
{
withIds: true, // enable automatic creation of id and htmlFor props
}
);
const { const [formErrors, setFormErrors] = useState<FormErrors>({});
errors: formStateErrors, const [formValidity, setFormValidity] = useState<FormValidity>({});
values: formStateValues,
validity: formStateValidity,
setField: setFormStateField,
setFieldError: setFormStateFieldError,
} = formState;
const isFormValid: boolean = const isFormValid: boolean =
((isUndefined(formStateValidity.apikey) || formStateValidity.apikey) && ((isUndefined(formValidity.apikey) || formValidity.apikey) &&
formStateValues.apikey.trim().length === 40 && // invalidate if api key is empty formValues.apikey.trim().length === 40 &&
isUndefined(formStateErrors.apikey) && isUndefined(formErrors.apikey) &&
(((isUndefined(formStateValidity.host) || formStateValidity.host) && (((isUndefined(formValidity.host) || formValidity.host) &&
isUndefined(formStateErrors.host)) || isUndefined(formErrors.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) !formValues.advanced)) ||
!formStateValues.advanced)) ||
false; false;
// on component mount -> save `settings` object // on component mount -> save `settings` object
useEffect(() => { useEffect(() => {
onSave({ onSave({
...formStateValues, ...formValues,
...(formStateValues.advanced === false && {host: ''}), ...(formValues.advanced === false && {host: ''}),
}); });
}, [formStateValues]); }, [formValues]);
function handleApiKeyInputChange(apikey: string): void { function handleApiKeyInputChange(apikey: string): void {
setFormStateField('apikey', apikey); setFormValues((prev) => ({...prev, apikey}));
// ToDo: Remove special symbols
if (!(apikey.trim().length > 0)) { 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) { } 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) { } 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 { function handleHostUrlInputChange(host: string): void {
if (!formStateValues.advanced) { if (!formValues.advanced) {
setFormStateFieldError('host', 'Enable Advanced Options first'); setFormErrors((prev) => ({...prev, host: 'Enable Advanced Options first'}));
setFormValidity((prev) => ({...prev, host: false}));
return; return;
} }
setFormStateField('host', host); setFormValues((prev) => ({...prev, host}));
if (!(host.trim().length > 0)) { 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; return;
} }
if (!isValidUrl(host.trim()) || host.trim().length < 10) { 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); setSubmitting(true);
// request API validation request // request API validation request
const apiKeyValidationBody: AuthRequestBodyProperties = { const apiKeyValidationBody: AuthRequestBodyProperties = {
apikey: formStateValues.apikey.trim(), apikey: formValues.apikey.trim(),
hostUrl: hostUrl:
(formStateValues.advanced && (formValues.advanced &&
formStateValues.host.trim().length > 0 && formValues.host.trim().length > 0 &&
formStateValues.host.trim()) || formValues.host.trim()) ||
Kutt.hostUrl, Kutt.hostUrl,
}; };
@@ -185,67 +171,64 @@ const Form: React.FC = () => {
return ( return (
<> <>
<div tw="mt-4"> <div className={styles.formSection}>
<div tw="flex flex-col text-sm"> <div className={styles.inputGroup}>
<label {...labelProps('apikey')} tw="mb-2 font-bold"> <label htmlFor="apikey" className={styles.label}>
API Key API Key
<small tw="tracking-normal lowercase"> <small>
<a <a
href={`${ href={`${
(formStateValues.advanced && formStateValues.host) || (formValues.advanced && formValues.host) ||
Kutt.hostUrl Kutt.hostUrl
}/login`} }/login`}
target="blank" target="blank"
rel="nofollow noopener noreferrer" rel="nofollow noopener noreferrer"
tw="ml-2 text-blue-500 no-underline" className={styles.labelLink}
> >
get one? get one?
</a> </a>
</small> </small>
</label> </label>
<div tw="relative"> <div className={styles.inputWrapper}>
<div tw="absolute top-0 right-0 flex w-10 h-full border border-transparent"> <div className={styles.inputIconWrapper}>
<Icon <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)} onClick={(): void => setShowApiKey(!showApiKey)}
name={!showApiKey ? 'eye-closed' : 'eye'} name={!showApiKey ? 'eye-closed' : 'eye'}
/> />
</div> </div>
<input <input
{...passwordProps('apikey')} id="apikey"
name="apikey"
type={!showApiKey ? 'password' : 'text'} type={!showApiKey ? 'password' : 'text'}
onChange={({ value={formValues.apikey}
target: {value}, onChange={(e: ChangeEvent<HTMLInputElement>): void => {
}: React.ChangeEvent<HTMLInputElement>): void => { handleApiKeyInputChange(e.target.value.trim());
// NOTE: overriding onChange to show errors
handleApiKeyInputChange(value.trim());
}} }}
spellCheck="false" spellCheck="false"
css={[ className={clsx(
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`, styles.input,
!isUndefined(formValidity.apikey) &&
!isUndefined(formStateValidity.apikey) && !formValidity.apikey &&
!formStateValidity.apikey && styles.inputError
tw`border-red-500`, )}
]}
/> />
</div> </div>
<span tw="flex items-center mt-1 ml-1 text-xs font-medium tracking-wide text-red-500"> <span className={styles.errorText}>{formErrors.apikey}</span>
{formStateErrors.apikey}
</span>
</div> </div>
</div> </div>
<div> <div>
<StyledValidateButton <button
type="submit" type="submit"
disabled={submitting || !isFormValid} disabled={submitting || !isFormValid}
onClick={handleApiKeyVerification} onClick={handleApiKeyVerification}
className={styles.validateButton}
> >
<span tw="ml-2">Validate</span> <span className={styles.validateText}>Validate</span>
<Icon <Icon
name={ name={
@@ -255,96 +238,93 @@ const Form: React.FC = () => {
((!errored.error && 'tick') || 'cross')) || ((!errored.error && 'tick') || 'cross')) ||
'zap' 'zap'
} }
className="icon validate__icon" className={styles.validateIcon}
/> />
</StyledValidateButton> </button>
</div> </div>
<div tw="flex flex-col mt-6"> <div className={styles.toggleSection}>
<label <label htmlFor="history" className={styles.toggleLabel}>
{...labelProps('history')} <span className={styles.toggleText}>Keep History</span>
tw="flex justify-between items-center mt-3 cursor-pointer"
>
<span tw="text-sm">Keep History</span>
<span tw="relative ml-3"> <span className={styles.toggleWrapper}>
<span tw="block w-10 h-6 bg-gray-400 rounded-full shadow-inner" /> <span className={styles.toggleTrack} />
<span <span
css={[ className={clsx(
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`, styles.toggleKnob,
formValues.history && styles.active
!formStateValues.history )}
? tw`bg-white`
: tw`transform translate-x-full bg-purple-600`,
]}
> >
<input <input
{...checkboxProps('history')} id="history"
tw="absolute w-0 h-0 opacity-0" name="history"
type="checkbox"
checked={formValues.history}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
setFormValues((prev) => ({...prev, history: e.target.checked}));
}}
className={styles.toggleInput}
/> />
</span> </span>
</span> </span>
</label> </label>
<label <label htmlFor="advanced" className={styles.toggleLabel}>
{...labelProps('advanced')} <span className={styles.toggleText}>Show Advanced Options</span>
tw="flex justify-between items-center mt-3 cursor-pointer"
>
<span tw="text-sm">Show Advanced Options</span>
<span tw="relative ml-3"> <span className={styles.toggleWrapper}>
<span tw="block w-10 h-6 bg-gray-400 rounded-full shadow-inner" /> <span className={styles.toggleTrack} />
<span <span
css={[ className={clsx(
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`, styles.toggleKnob,
formValues.advanced && styles.active
!formStateValues.advanced )}
? tw`bg-white`
: tw`transform translate-x-full bg-purple-600`,
]}
> >
<input <input
{...checkboxProps('advanced')} id="advanced"
tw="absolute w-0 h-0 opacity-0" name="advanced"
type="checkbox"
checked={formValues.advanced}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
setFormValues((prev) => ({...prev, advanced: e.target.checked}));
}}
className={styles.toggleInput}
/> />
</span> </span>
</span> </span>
</label> </label>
<div css={[tw`mt-4`, !formStateValues.advanced && tw`invisible`]}> <div className={clsx(styles.advancedSection, !formValues.advanced && styles.hidden)}>
<div tw="flex flex-col text-sm"> <div className={styles.inputGroup}>
<label {...labelProps('host')} tw="mb-2 font-bold"> <label htmlFor="host" className={styles.label}>
Custom Host Custom Host
</label> </label>
<div tw="relative"> <div className={styles.inputWrapper}>
<input <input
{...textProps('host')} id="host"
onChange={({ name="host"
target: {value}, type="text"
}: React.ChangeEvent<HTMLInputElement>): void => { value={formValues.host}
// NOTE: overriding onChange to show errors onChange={(e: ChangeEvent<HTMLInputElement>): void => {
handleHostUrlInputChange(value.trim()); handleHostUrlInputChange(e.target.value.trim());
}} }}
spellCheck="false" spellCheck="false"
css={[ className={clsx(
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`, styles.input,
!isUndefined(formValidity.host) &&
!isUndefined(formStateValidity.host) && !formValidity.host &&
!formStateValidity.host && styles.inputError
tw`border-red-500`, )}
]}
/> />
</div> </div>
<span tw="flex items-center mt-1 ml-1 text-xs font-medium tracking-wide text-red-500"> <span className={styles.errorText}>{formErrors.host}</span>
{formStateErrors.host}
</span>
</div> </div>
</div> </div>
</div> </div>
</> </>
); );
}; }
export default Form; export default Form;

View 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;
}

View File

@@ -1,22 +1,23 @@
import React from 'react'; import {memo} from 'react';
import 'twin.macro';
const Header: React.FC = () => { import styles from './Header.module.scss';
function Header() {
return ( return (
<> <>
<header tw="flex items-center justify-center pb-4"> <header className={styles.header}>
<img <img
tw="w-8 h-8" className={styles.logo}
width="32" width="32"
height="32" height="32"
src="assets/logo.png" src="../assets/logo.png"
alt="logo" alt="logo"
/> />
<h1 tw="font-medium text-3xl ml-1 text-center mb-0">Kutt</h1> <h1 className={styles.title}>Kutt</h1>
</header> </header>
</> </>
); );
}; }
export default React.memo(Header); export default memo(Header);

View 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;
}

View File

@@ -1,5 +1,4 @@
import React, {useEffect} from 'react'; import {useEffect} from 'react';
import 'twin.macro';
import {getExtensionSettings} from '../util/settings'; import {getExtensionSettings} from '../util/settings';
import { import {
@@ -20,7 +19,9 @@ import Header from './Header';
import Footer from './Footer'; import Footer from './Footer';
import Form from './Form'; import Form from './Form';
const Options: React.FC = () => { import styles from './Options.module.scss';
function Options() {
const extensionSettingsDispatch = useExtensionSettings()[1]; const extensionSettingsDispatch = useExtensionSettings()[1];
const [requestStatusState, requestStatusDispatch] = useRequestStatus(); const [requestStatusState, requestStatusDispatch] = useRequestStatus();
@@ -38,7 +39,7 @@ const Options: React.FC = () => {
.replace('http://', '') .replace('http://', '')
.replace('https://', '') .replace('https://', '')
.replace('www.', '') .replace('www.', '')
.split(/[/?#]/)[0], // extract domain .split(/[/?#]/)[0] || '', // extract domain
hostUrl: (settings.host as string).endsWith('/') hostUrl: (settings.host as string).endsWith('/')
? (settings.host as string).slice(0, -1) ? (settings.host as string).slice(0, -1)
: (settings.host as string), // slice `/` at the end : (settings.host as string), // slice `/` at the end
@@ -70,20 +71,14 @@ const Options: React.FC = () => {
return ( return (
<> <>
<BodyWrapper> <BodyWrapper>
<div <div id="options" className={styles.optionsPage}>
id="options" <div className={styles.optionsContainer}>
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'}
>
<Header /> <Header />
{!requestStatusState.loading ? ( {!requestStatusState.loading ? (
<Form /> <Form />
) : ( ) : (
<div tw="h-64"> <div className={styles.loaderContainer}>
<Loader /> <Loader />
</div> </div>
)} )}
@@ -94,6 +89,6 @@ const Options: React.FC = () => {
</BodyWrapper> </BodyWrapper>
</> </>
); );
}; }
export default Options; export default Options;

View File

@@ -1,25 +1,24 @@
import {ThemeProvider} from 'styled-components'; import {StrictMode} from 'react';
import ReactDOM from 'react-dom'; import {createRoot} from 'react-dom/client';
import React from 'react';
// Common styles
import '../styles/main.scss';
import {ExtensionSettingsProvider} from '../contexts/extension-settings-context'; import {ExtensionSettingsProvider} from '../contexts/extension-settings-context';
import {RequestStatusProvider} from '../contexts/request-status-context'; import {RequestStatusProvider} from '../contexts/request-status-context';
import Options from './Options'; 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 import '../styles/main.scss';
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( const container = document.getElementById('options-root');
<ThemeProvider theme={theme}> if (!container) {
throw new Error('Could not find options-root container');
}
const root = createRoot(container);
root.render(
<StrictMode>
<ExtensionSettingsProvider> <ExtensionSettingsProvider>
<RequestStatusProvider> <RequestStatusProvider>
<Options /> <Options />
</RequestStatusProvider> </RequestStatusProvider>
</ExtensionSettingsProvider> </ExtensionSettingsProvider>
</ThemeProvider>, </StrictMode>
document.getElementById('options-root')
); );

View File

@@ -3,9 +3,10 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Options</title> <title>Options: Kutt</title>
</head> </head>
<body> <body>
<div id="options-root"></div> <div id="options-root"></div>
<script type="module" src="./index.tsx"></script>
</body> </body>
</html> </html>

View 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;
}
}

View File

@@ -1,13 +1,11 @@
import {useFormState} from 'react-use-form-state'; import {useState, type ChangeEvent} from 'react';
import tw, {css, styled} from 'twin.macro';
import React, {useState} from 'react';
import { import {
EMPTY_STRING, EMPTY_STRING,
isUndefined,
isEmpty, isEmpty,
isNull, isNull,
get, get,
} from '@abhijithvijayan/ts-utils'; } from '@abhijithvijayan/ts-utils';
import clsx from 'clsx';
import {useExtensionSettings} from '../contexts/extension-settings-context'; import {useExtensionSettings} from '../contexts/extension-settings-context';
import {SHORTEN_URL} from '../Background/constants'; import {SHORTEN_URL} from '../Background/constants';
@@ -26,30 +24,13 @@ import {
} from '../Background'; } from '../Background';
import Icon from '../components/Icon'; import Icon from '../components/Icon';
import styles from './Form.module.scss';
export enum CONSTANTS { export enum CONSTANTS {
DefaultDomainId = 'default', DefaultDomainId = 'default',
} }
const StyledValidateButton = styled.button` function Form() {
${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 = () => {
const extensionSettingsState = useExtensionSettings()[0]; const extensionSettingsState = useExtensionSettings()[0];
const requestStatusDispatch = useRequestStatus()[1]; const requestStatusDispatch = useRequestStatus()[1];
const [showPassword, setShowPassword] = useState<boolean>(false); const [showPassword, setShowPassword] = useState<boolean>(false);
@@ -59,66 +40,33 @@ const Form: React.FC = () => {
host: {hostDomain}, host: {hostDomain},
} = extensionSettingsState; } = extensionSettingsState;
const [ const [formState, setFormState] = useState({
formState,
{
text: textProps,
password: passwordProps,
select: selectProps,
label: labelProps,
},
] = useFormState<{
domain: string;
customurl: string;
password: string;
}>(
{
domain: domain:
domainOptions domainOptions
.find(({id}) => { .find(({id}) => id === CONSTANTS.DefaultDomainId)
return id === CONSTANTS.DefaultDomainId; ?.value?.trim() || EMPTY_STRING,
}) customurl: '',
?.value?.trim() || EMPTY_STRING, // empty string will map to disabled entry password: '',
}, });
{ const [formErrors, setFormErrors] = useState<{
withIds: true, // enable automatic creation of id and htmlFor props customurl?: string;
} password?: string;
); }>({});
const {
errors: formStateErrors,
validity: formStateValidity,
setField: setFormStateField,
setFieldError: setFormStateFieldError,
} = formState;
const isFormValid: boolean = const isFormValid: boolean =
((isUndefined(formStateValidity.customurl) || !formErrors.customurl &&
formStateValidity.customurl) && !formErrors.password &&
(isUndefined(formStateValidity.password) || formStateValidity.password) && true;
isUndefined(formStateErrors.customurl) &&
isUndefined(formStateErrors.password)) ||
false;
async function handleFormSubmit({ async function handleFormSubmit(): Promise<void> {
customurl,
password,
domain,
}: {
domain: string;
customurl: string;
password: string;
}): Promise<void> {
// enable loading screen
setIsSubmitting(true); setIsSubmitting(true);
// Get target link to shorten
const tabs = await getCurrentTab(); const tabs = await getCurrentTab();
const target: string | null = get(tabs, '[0].url', null); const target: string | null = get(tabs, '[0].url', null);
const shouldSubmit: boolean = !isNull(target) && isValidUrl(target); const shouldSubmit: boolean = !isNull(target) && isValidUrl(target);
if (!shouldSubmit) { if (!shouldSubmit) {
setIsSubmitting(false); setIsSubmitting(false);
requestStatusDispatch({ requestStatusDispatch({
type: RequestStatusActionTypes.SET_REQUEST_STATUS, type: RequestStatusActionTypes.SET_REQUEST_STATUS,
payload: { payload: {
@@ -126,35 +74,36 @@ const Form: React.FC = () => {
message: 'Not a valid URL', message: 'Not a valid URL',
}, },
}); });
return; return;
} }
const apiBody: ApiBodyProperties = { const apiBody: ApiBodyProperties = {
apikey: extensionSettingsState.apikey, apikey: extensionSettingsState.apikey,
target: target as unknown as string, target: target as unknown as string,
...(customurl.trim() !== EMPTY_STRING && {customurl: customurl.trim()}), // add key only if field is not empty ...(formState.customurl.trim() !== EMPTY_STRING && {
...(!isEmpty(password) && {password}), customurl: formState.customurl.trim(),
}),
...(!isEmpty(formState.password) && {password: formState.password}),
reuse: false, reuse: false,
...(domain.trim() !== EMPTY_STRING && {domain: domain.trim()}), ...(formState.domain.trim() !== EMPTY_STRING && {
domain: formState.domain.trim(),
}),
}; };
const apiShortenUrlBody: ShortUrlActionBodyProperties = { const apiShortenUrlBody: ShortUrlActionBodyProperties = {
apiBody, apiBody,
hostUrl: extensionSettingsState.host.hostUrl, hostUrl: extensionSettingsState.host.hostUrl,
}; };
// shorten url in the background
const response: SuccessfulShortenStatusProperties | ApiErroredProperties = const response: SuccessfulShortenStatusProperties | ApiErroredProperties =
await messageUtil.send(SHORTEN_URL, apiShortenUrlBody); await messageUtil.send(SHORTEN_URL, apiShortenUrlBody);
// disable spinner
setIsSubmitting(false); setIsSubmitting(false);
if (!response.error) { if (!response.error) {
const { const {
data: {link}, data: {link},
} = response; } = response;
// show shortened url
requestStatusDispatch({ requestStatusDispatch({
type: RequestStatusActionTypes.SET_REQUEST_STATUS, type: RequestStatusActionTypes.SET_REQUEST_STATUS,
payload: { payload: {
@@ -163,7 +112,6 @@ const Form: React.FC = () => {
}, },
}); });
} else { } else {
// errored
requestStatusDispatch({ requestStatusDispatch({
type: RequestStatusActionTypes.SET_REQUEST_STATUS, type: RequestStatusActionTypes.SET_REQUEST_STATUS,
payload: { payload: {
@@ -175,118 +123,89 @@ const Form: React.FC = () => {
} }
function handleCustomUrlInputChange(url: string): void { function handleCustomUrlInputChange(url: string): void {
setFormStateField('customurl', url); setFormState((prev) => ({...prev, customurl: url}));
// ToDo: Remove special symbols
if (url.length > 0 && url.length < 3) { if (url.length > 0 && url.length < 3) {
setFormStateFieldError( setFormErrors((prev) => ({
'customurl', ...prev,
'Custom URL must be at-least 3 characters' customurl: 'Custom URL must be at-least 3 characters',
); }));
} else {
setFormErrors((prev) => ({...prev, customurl: undefined}));
} }
} }
function handlePasswordInputChange(password: string): void { function handlePasswordInputChange(password: string): void {
setFormStateField('password', password); setFormState((prev) => ({...prev, password}));
// ToDo: Remove special symbols
if (password.length > 0 && password.length < 3) { if (password.length > 0 && password.length < 3) {
setFormStateFieldError( setFormErrors((prev) => ({
'password', ...prev,
'Password must be at-least 3 characters' password: 'Password must be at-least 3 characters',
); }));
} else {
setFormErrors((prev) => ({...prev, password: undefined}));
} }
} }
return ( return (
<> <div className={styles.formContainer}>
<div tw="flex flex-col w-full max-w-sm p-4 mx-auto bg-white select-none"> <div className={styles.formGroup}>
<div tw="flex flex-col mb-4"> <label htmlFor="domain" className={styles.label}>
<label
{...labelProps('domain')}
tw="sm:text-sm mb-1 text-xs tracking-wide text-gray-600"
>
Domain Domain
</label> </label>
<div tw="relative"> <div className={styles.selectWrapper}>
<select <select
{...selectProps('domain')} id="domain"
name="domain"
value={formState.domain}
onChange={(e) =>
setFormState((prev) => ({...prev, domain: e.target.value}))
}
disabled={isSubmitting} disabled={isSubmitting}
css={[ className={styles.select}
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`,
]}
> >
{domainOptions.map(({id, option, value, disabled = false}) => { {domainOptions.map(({id, option, value, disabled = false}) => (
return (
<option <option
tw="bg-gray-200 " className={styles.selectOption}
value={value} value={value}
disabled={disabled} disabled={disabled}
key={id} key={id}
> >
{option} {option}
</option> </option>
); ))}
})}
</select> </select>
</div> </div>
</div> </div>
<div tw="flex flex-col mb-3 relative"> <div className={styles.formGroupRelative}>
<label <label htmlFor="customurl" className={styles.labelAbsolute}>
{...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"
>
<span>{hostDomain}/</span> <span>{hostDomain}/</span>
</label> </label>
<input <input
{...textProps('customurl')} id="customurl"
onChange={({ name="customurl"
target: {value}, type="text"
}: React.ChangeEvent<HTMLInputElement>): void => { value={formState.customurl}
// NOTE: overriding onChange to show errors onChange={(e: ChangeEvent<HTMLInputElement>): void => {
handleCustomUrlInputChange(value.trim()); handleCustomUrlInputChange(e.target.value.trim());
}} }}
disabled={isSubmitting} disabled={isSubmitting}
spellCheck="false" spellCheck="false"
css={[ className={clsx(styles.input, formErrors.customurl && styles.inputError)}
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`,
]}
/> />
<span tw="flex items-center mt-1 ml-1 text-xs font-medium tracking-wide text-red-500"> <span className={styles.errorText}>{formErrors.customurl}</span>
{formStateErrors.customurl}
</span>
</div> </div>
<div tw="flex flex-col mb-3 relative"> <div className={styles.formGroupRelative}>
<label <label htmlFor="password" className={styles.labelAbsolute}>
{...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"
>
<span>Password</span> <span>Password</span>
</label> </label>
<div tw="relative"> <div className={styles.passwordWrapper}>
<div <div className={styles.passwordToggle}>
css={[
tw`absolute top-0 right-0 flex w-10 mt-6 border border-transparent`,
css`
margin-top: 1.75rem;
`,
]}
>
<Icon <Icon
onClick={(): void => { onClick={(): void => {
if (!isSubmitting) { if (!isSubmitting) {
@@ -294,61 +213,41 @@ const Form: React.FC = () => {
} }
}} }}
name={!showPassword ? 'eye-closed' : 'eye'} name={!showPassword ? 'eye-closed' : 'eye'}
css={[ className={styles.passwordToggleIcon}
tw`z-10 flex items-center justify-center w-full h-full rounded-tl rounded-bl cursor-pointer`,
css`
color: rgb(187, 187, 187);
`,
]}
/> />
</div> </div>
<input <input
{...passwordProps('password')} id="password"
name="password"
type={!showPassword ? 'password' : 'text'} type={!showPassword ? 'password' : 'text'}
value={formState.password}
spellCheck="false" spellCheck="false"
onChange={({ onChange={(e: ChangeEvent<HTMLInputElement>): void => {
target: {value}, handlePasswordInputChange(e.target.value);
}: React.ChangeEvent<HTMLInputElement>): void => {
// NOTE: overriding onChange to show errors
handlePasswordInputChange(value);
}} }}
disabled={isSubmitting} disabled={isSubmitting}
css={[ className={clsx(styles.input, formErrors.password && styles.inputError)}
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`,
]}
/> />
</div> </div>
<span tw="flex items-center mt-1 ml-1 text-xs font-medium tracking-wide text-red-500"> <span className={styles.errorText}>{formErrors.password}</span>
{formStateErrors.password}
</span>
</div> </div>
<StyledValidateButton <button
type="submit" type="submit"
disabled={!isFormValid || isSubmitting} disabled={!isFormValid || isSubmitting}
onClick={(): void => { onClick={handleFormSubmit}
handleFormSubmit(formState.values); className={styles.submitButton}
}}
> >
{!isSubmitting ? ( {!isSubmitting ? (
<span>Create</span> <span>Create</span>
) : ( ) : (
<Icon className="icon create__icon" name="spinner" /> <Icon className={styles.createIcon} name="spinner" />
)} )}
</StyledValidateButton> </button>
</div> </div>
</>
); );
}; }
export default Form; export default Form;

View 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;
}
}

View File

@@ -1,6 +1,6 @@
import {isNull, EMPTY_STRING} from '@abhijithvijayan/ts-utils'; import {isNull, EMPTY_STRING} from '@abhijithvijayan/ts-utils';
import React, {useState} from 'react'; import {useState} from 'react';
import tw, {styled} from 'twin.macro'; import clsx from 'clsx';
import {openExtOptionsPage, openHistoryPage} from '../util/tabs'; import {openExtOptionsPage, openHistoryPage} from '../util/tabs';
import {updateExtensionSettings} from '../util/settings'; import {updateExtensionSettings} from '../util/settings';
@@ -18,14 +18,9 @@ import {
} from '../Background'; } from '../Background';
import Icon from '../components/Icon'; import Icon from '../components/Icon';
import styles from './Header.module.scss';
const StyledIcon = styled(Icon)` function Header() {
${tw`hover:opacity-75 bg-transparent shadow-none`}
color: rgb(187, 187, 187);
`;
const Header: React.FC = () => {
const [extensionSettingsState, extensionSettingsDispatch] = const [extensionSettingsState, extensionSettingsDispatch] =
useExtensionSettings(); useExtensionSettings();
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
@@ -35,7 +30,6 @@ const Header: React.FC = () => {
}); });
async function fetchUserDomains(): Promise<void> { async function fetchUserDomains(): Promise<void> {
// show loading spinner
setLoading(true); setLoading(true);
const apiKeyValidationBody: AuthRequestBodyProperties = { const apiKeyValidationBody: AuthRequestBodyProperties = {
@@ -43,36 +37,26 @@ const Header: React.FC = () => {
hostUrl: extensionSettingsState.host.hostUrl, hostUrl: extensionSettingsState.host.hostUrl,
}; };
// request API
const response: SuccessfulApiKeyCheckProperties | ApiErroredProperties = const response: SuccessfulApiKeyCheckProperties | ApiErroredProperties =
await messageUtil.send(CHECK_API_KEY, apiKeyValidationBody); await messageUtil.send(CHECK_API_KEY, apiKeyValidationBody);
// stop spinner
setLoading(false); setLoading(false);
if (!response.error) { if (!response.error) {
// ---- success ---- //
setErrored({error: false, message: 'Fetching domains successful'}); setErrored({error: false, message: 'Fetching domains successful'});
// Store user account information
const {domains, email} = response.data; const {domains, email} = response.data;
await updateExtensionSettings({user: {domains, email}}); await updateExtensionSettings({user: {domains, email}});
} else { } else {
// ---- errored ---- //
setErrored({error: true, message: response.message}); setErrored({error: true, message: response.message});
// Delete `user` field from settings
await updateExtensionSettings({user: null}); await updateExtensionSettings({user: null});
} }
// hot reload page(read from localstorage and update state)
extensionSettingsDispatch({ extensionSettingsDispatch({
type: ExtensionSettingsActionTypes.RELOAD_EXTENSION_SETTINGS, type: ExtensionSettingsActionTypes.RELOAD_EXTENSION_SETTINGS,
payload: !extensionSettingsState.reload, payload: !extensionSettingsState.reload,
}); });
setTimeout(() => { setTimeout(() => {
// Reset status
setErrored({error: null, message: EMPTY_STRING}); setErrored({error: null, message: EMPTY_STRING});
}, 1000); }, 1000);
} }
@@ -83,43 +67,41 @@ const Header: React.FC = () => {
'refresh'; 'refresh';
return ( return (
<> <header className={styles.header}>
<header tw="flex items-center justify-between p-4 select-none">
<div> <div>
<img <img
tw="w-8 h-8" className={styles.logo}
width="32" width="32"
height="32" height="32"
src="assets/logo.png" src="../assets/logo.png"
alt="logo" alt="logo"
/> />
</div> </div>
<div tw="flex"> <div className={styles.actions}>
<StyledIcon <Icon
onClick={fetchUserDomains} onClick={fetchUserDomains}
name={iconToShow} name={iconToShow}
title="Refresh" title="Refresh"
className="icon" className={clsx('icon', styles.styledIcon)}
/> />
{extensionSettingsState.history && ( {extensionSettingsState.history && (
<StyledIcon <Icon
onClick={openHistoryPage} onClick={openHistoryPage}
name="clock" name="clock"
className="icon" className={clsx('icon', styles.styledIcon)}
title="History" title="History"
/> />
)} )}
<StyledIcon <Icon
onClick={openExtOptionsPage} onClick={openExtOptionsPage}
name="settings" name="settings"
className="icon" className={clsx('icon', styles.styledIcon)}
title="Settings" title="Settings"
/> />
</div> </div>
</header> </header>
</>
); );
}; }
export default Header; export default Header;

View File

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

View File

@@ -1,6 +1,5 @@
import {isNull, EMPTY_STRING} from '@abhijithvijayan/ts-utils'; import {isNull, EMPTY_STRING} from '@abhijithvijayan/ts-utils';
import React, {useEffect} from 'react'; import {useEffect} from 'react';
import tw, {css} from 'twin.macro';
import {Kutt, UserSettingsResponseProperties} from '../Background'; import {Kutt, UserSettingsResponseProperties} from '../Background';
import {openExtOptionsPage} from '../util/tabs'; import {openExtOptionsPage} from '../util/tabs';
@@ -28,7 +27,9 @@ import PopupHeader from './Header';
import Loader from '../components/Loader'; import Loader from '../components/Loader';
import Form, {CONSTANTS} from './Form'; import Form, {CONSTANTS} from './Form';
const Popup: React.FC = () => { import styles from './Popup.module.scss';
function Popup() {
const [extensionSettingsState, extensionSettingsDispatch] = const [extensionSettingsState, extensionSettingsDispatch] =
useExtensionSettings(); useExtensionSettings();
const [requestStatusState, requestStatusDispatch] = useRequestStatus(); const [requestStatusState, requestStatusDispatch] = useRequestStatus();
@@ -37,12 +38,10 @@ const Popup: React.FC = () => {
// re-renders on `liveReloadFlag` change // re-renders on `liveReloadFlag` change
useEffect((): void => { useEffect((): void => {
async function getUserSettings(): Promise<void> { async function getUserSettings(): Promise<void> {
// -----------------------------------------------------------------------------//
// -----------------------------------------------------------------------------// // -----------------------------------------------------------------------------//
// ----- // ToDo: remove in next major release // ----- // // ----- // ToDo: remove in next major release // ----- //
// ----- Ref: https://github.com/thedevs-network/kutt-extension/issues/78 ----- // // ----- Ref: https://github.com/thedevs-network/kutt-extension/issues/78 ----- //
// -----------------------------------------------------------------------------// // -----------------------------------------------------------------------------//
// -----------------------------------------------------------------------------//
const { const {
// old keys from extension v3.x.x // old keys from extension v3.x.x
@@ -61,7 +60,6 @@ const Popup: React.FC = () => {
let performMigration = false; let performMigration = false;
if ((key as string).trim().length > 0) { if ((key as string).trim().length > 0) {
// map it to `settings.apikey`
migrationSettings.apikey = key; migrationSettings.apikey = key;
performMigration = true; performMigration = true;
} }
@@ -69,30 +67,20 @@ const Popup: React.FC = () => {
(host as string).trim().length > 0 && (host as string).trim().length > 0 &&
(userOptions.devMode as boolean) (userOptions.devMode as boolean)
) { ) {
// map `host` to `settings.host`
migrationSettings.host = host; migrationSettings.host = host;
// set `advanced` to true
migrationSettings.advanced = true; migrationSettings.advanced = true;
performMigration = true; performMigration = true;
} }
if (userOptions.keepHistory as boolean) { if (userOptions.keepHistory as boolean) {
// set `settings.history` to true
migrationSettings.history = true; migrationSettings.history = true;
performMigration = true; performMigration = true;
} }
if (performMigration) { if (performMigration) {
// perform migration
await migrateSettings(migrationSettings); await migrateSettings(migrationSettings);
} }
// -----------------------------------------------------------------------------//
// -----------------------------------------------------------------------------//
// -----------------------------------------------------------------------------//
// -----------------------------------------------------------------------------//
// -----------------------------------------------------------------------------//
// -----------------------------------------------------------------------------// // -----------------------------------------------------------------------------//
// ToDo: set types: refer https://kutt.it/jITyIU
const {settings = {}} = await getExtensionSettings(); const {settings = {}} = await getExtensionSettings();
// No API Key set // No API Key set
@@ -138,16 +126,15 @@ const Popup: React.FC = () => {
.replace('http://', EMPTY_STRING) .replace('http://', EMPTY_STRING)
.replace('https://', EMPTY_STRING) .replace('https://', EMPTY_STRING)
.replace('www.', EMPTY_STRING) .replace('www.', EMPTY_STRING)
.split(/[/?#]/)[0], // extract domain .split(/[/?#]/)[0] || EMPTY_STRING,
hostUrl: (settings.host as string).endsWith('/') hostUrl: (settings.host as string).endsWith('/')
? (settings.host as string).slice(0, -1) ? (settings.host as string).slice(0, -1)
: (settings.host as string), // slice `/` at the end : (settings.host as string),
}; };
} }
} }
let historyEnabled = false; let historyEnabled = false;
// `history` field set
if ( if (
Object.prototype.hasOwnProperty.call(settings, 'history') && Object.prototype.hasOwnProperty.call(settings, 'history') &&
(settings.history as boolean) (settings.history as boolean)
@@ -189,10 +176,8 @@ const Popup: React.FC = () => {
} }
); );
// merge to beginning of array
optionsList = defaultOptions.concat(optionsList); optionsList = defaultOptions.concat(optionsList);
// update domain list
extensionSettingsDispatch({ extensionSettingsDispatch({
type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS, type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS,
payload: { payload: {
@@ -203,7 +188,6 @@ const Popup: React.FC = () => {
}, },
}); });
} else { } else {
// no `user` but `apikey` exist on storage
extensionSettingsDispatch({ extensionSettingsDispatch({
type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS, type: ExtensionSettingsActionTypes.HYDRATE_EXTENSION_SETTINGS,
payload: { payload: {
@@ -215,7 +199,6 @@ const Popup: React.FC = () => {
}); });
} }
// stop loader
requestStatusDispatch({ requestStatusDispatch({
type: RequestStatusActionTypes.SET_LOADING, type: RequestStatusActionTypes.SET_LOADING,
payload: false, payload: false,
@@ -227,16 +210,7 @@ const Popup: React.FC = () => {
return ( return (
<BodyWrapper> <BodyWrapper>
<div <div id="popup" className={styles.popup}>
id="popup"
css={[
tw`text-lg`,
css`
min-height: 350px;
min-width: 270px;
`,
]}
>
{!requestStatusState.loading ? ( {!requestStatusState.loading ? (
<> <>
<PopupHeader /> <PopupHeader />
@@ -249,6 +223,6 @@ const Popup: React.FC = () => {
</div> </div>
</BodyWrapper> </BodyWrapper>
); );
}; }
export default Popup; export default Popup;

View 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;
}

View File

@@ -1,44 +1,27 @@
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import React, {useState, useEffect} from 'react'; import {useState, useEffect} from 'react';
import tw, {styled, css} from 'twin.macro'; import {QRCodeSVG} from 'qrcode.react';
import QRCode from 'qrcode.react'; import clsx from 'clsx';
import {useRequestStatus} from '../contexts/request-status-context'; import {useRequestStatus} from '../contexts/request-status-context';
import {removeProtocol} from '../util/link'; import {removeProtocol} from '../util/link';
import Icon from '../components/Icon'; import Icon from '../components/Icon';
import styles from './ResponseBody.module.scss';
export type ProcessedRequestProperties = { export type ProcessedRequestProperties = {
error: boolean | null; error: boolean | null;
message: string; message: string;
}; };
const StyledPopupBody = styled.div` function ResponseBody() {
${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 = () => {
const [{error, message}] = useRequestStatus(); const [{error, message}] = useRequestStatus();
const [copied, setCopied] = useState<boolean>(false); const [copied, setCopied] = useState<boolean>(false);
const [QRView, setQRView] = useState<boolean>(false); const [QRView, setQRView] = useState<boolean>(false);
// reset copy message // reset copy message
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout | null = null; let timer: ReturnType<typeof setTimeout> | null = null;
timer = setTimeout(() => { timer = setTimeout(() => {
setCopied(false); setCopied(false);
@@ -53,18 +36,11 @@ const ResponseBody: React.FC = () => {
return ( return (
<> <>
<StyledPopupBody> <div className={styles.popupBody}>
{!error ? ( {!error ? (
<> <>
<Icon <Icon
className="icon" className={clsx(styles.icon, styles.qrIcon)}
css={[
tw`my-0 ml-0`,
css`
margin-right: 0.4rem;
`,
]}
name="qrcode" name="qrcode"
onClick={(): void => { onClick={(): void => {
return setQRView(!QRView); return setQRView(!QRView);
@@ -78,10 +54,10 @@ const ResponseBody: React.FC = () => {
return setCopied(true); 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> </CopyToClipboard>
) : ( ) : (
<Icon tw="my-0 ml-0 mr-3" className="icon" name="tick" /> <Icon className={clsx(styles.icon, styles.copyIcon)} name="tick" />
)} )}
<CopyToClipboard <CopyToClipboard
@@ -90,23 +66,21 @@ const ResponseBody: React.FC = () => {
return setCopied(true); return setCopied(true);
}} }}
> >
<h1>{removeProtocol(message)}</h1> <h1 className={styles.link}>{removeProtocol(message)}</h1>
</CopyToClipboard> </CopyToClipboard>
</> </>
) : ( ) : (
<p tw="pt-1 text-lg text-gray-900 border-b border-gray-700 border-dotted"> <p className={styles.errorMessage}>{message}</p>
{message}
</p>
)} )}
</StyledPopupBody> </div>
{!error && QRView && ( {!error && QRView && (
<div tw="flex justify-center max-w-full pt-4 pb-0"> <div className={styles.qrCodeContainer}>
<QRCode size={128} value={message} /> <QRCodeSVG size={128} value={message} />
</div> </div>
)} )}
</> </>
); );
}; }
export default ResponseBody; export default ResponseBody;

View File

@@ -1,25 +1,24 @@
import {ThemeProvider} from 'styled-components'; import {StrictMode} from 'react';
import React from 'react'; import {createRoot} from 'react-dom/client';
import ReactDOM from 'react-dom';
// Common styles
import '../styles/main.scss';
import {ExtensionSettingsProvider} from '../contexts/extension-settings-context'; import {ExtensionSettingsProvider} from '../contexts/extension-settings-context';
import {RequestStatusProvider} from '../contexts/request-status-context'; import {RequestStatusProvider} from '../contexts/request-status-context';
import Popup from './Popup'; 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 import '../styles/main.scss';
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( const container = document.getElementById('popup-root');
<ThemeProvider theme={theme}> if (!container) {
throw new Error('Could not find popup-root container');
}
const root = createRoot(container);
root.render(
<StrictMode>
<ExtensionSettingsProvider> <ExtensionSettingsProvider>
<RequestStatusProvider> <RequestStatusProvider>
<Popup /> <Popup />
</RequestStatusProvider> </RequestStatusProvider>
</ExtensionSettingsProvider> </ExtensionSettingsProvider>
</ThemeProvider>, </StrictMode>
document.getElementById('popup-root')
); );

View File

@@ -3,9 +3,10 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=500" /> <meta name="viewport" content="width=500" />
<title>Popup</title> <title>Kutt</title>
</head> </head>
<body> <body>
<div id="popup-root"></div> <div id="popup-root"></div>
<script type="module" src="./index.tsx"></script>
</body> </body>
</html> </html>

View File

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

View File

@@ -1,19 +1,12 @@
import React from 'react'; import type {ReactNode} from 'react';
import 'twin.macro'; import styles from './BodyWrapper.module.scss';
type WrapperProperties = { type WrapperProperties = {
children: React.ReactChild; children: ReactNode;
}; };
const BodyWrapper: React.FC<WrapperProperties> = ({children}) => { function BodyWrapper({children}: WrapperProperties) {
// ToDo: get from props return <div className={styles.wrapper}>{children}</div>;
const isLoading = false; }
return (
<>
<div tw="w-full">{isLoading ? 'Loading...' : children}</div>
</>
);
};
export default BodyWrapper; export default BodyWrapper;

View File

@@ -1,7 +1,5 @@
import React from 'react'; import React from 'react';
import './styles.scss';
const Spinner: React.FC = () => { const Spinner: React.FC = () => {
return ( return (
<> <>

View File

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

View File

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

View File

@@ -1,25 +1,12 @@
import React from 'react';
import tw, {css} from 'twin.macro';
import Icon from './Icon'; import Icon from './Icon';
import styles from './Loader.module.scss';
const Loader: React.FC = (props) => { function Loader() {
return ( return (
<div <div className={styles.loader}>
css={[
tw`fixed flex items-center justify-center h-full`,
css`
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`,
]}
{...props}
>
<Icon name="spinner" /> <Icon name="spinner" />
</div> </div>
); );
}; }
export default Loader; export default Loader;

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* 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'; import {Kutt} from '../Background';
@@ -116,12 +116,12 @@ function useExtensionSettings(): [State, Dispatch] {
} }
type ExtensionSettingsProviderProps = { type ExtensionSettingsProviderProps = {
children: React.ReactNode; children: ReactNode;
}; };
const ExtensionSettingsProvider: React.FC<ExtensionSettingsProviderProps> = ({ function ExtensionSettingsProvider({
children, children,
}) => { }: ExtensionSettingsProviderProps) {
const [state, dispatch] = useReducer(extensionSettingsReducer, initialValues); const [state, dispatch] = useReducer(extensionSettingsReducer, initialValues);
return ( return (

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import React, {createContext, useReducer, useContext} from 'react'; import {createContext, useReducer, useContext, type ReactNode} from 'react';
export enum RequestStatusActionTypes { export enum RequestStatusActionTypes {
SET_REQUEST_STATUS = 'set-request-status', SET_REQUEST_STATUS = 'set-request-status',
@@ -86,12 +86,10 @@ function useRequestStatus(): [State, Dispatch] {
} }
type RequestStatusProviderProps = { type RequestStatusProviderProps = {
children: React.ReactNode; children: ReactNode;
}; };
const RequestStatusProvider: React.FC<RequestStatusProviderProps> = ({ function RequestStatusProvider({children}: RequestStatusProviderProps) {
children,
}) => {
const [state, dispatch] = useReducer(requestStatusReducer, initialValues); const [state, dispatch] = useReducer(requestStatusReducer, initialValues);
return ( return (

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* 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'; import {UserShortenedLinkStats} from '../Background';
@@ -92,7 +92,11 @@ function useShortenedLinks(): [State, Dispatch] {
return [useShortenedLinksContextState(), useShortenedLinksContextDispatch()]; return [useShortenedLinksContextState(), useShortenedLinksContextDispatch()];
} }
const ShortenedLinksProvider: React.FC = ({children}) => { type ShortenedLinksProviderProps = {
children: ReactNode;
};
function ShortenedLinksProvider({children}: ShortenedLinksProviderProps) {
const [state, dispatch] = useReducer(shortenedLinksReducer, initialValues); const [state, dispatch] = useReducer(shortenedLinksReducer, initialValues);
return ( return (

12
source/globals.d.ts vendored Normal file
View 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;
}

View File

@@ -1,67 +1,58 @@
{ {
"manifest_version": 2, "manifest_version": 3,
"name": "Kutt", "name": "Kutt",
"version": "0.0.0", "version": "0.0.0",
"short_name": "Kutt", "short_name": "Kutt",
"description": "Shorten long URLs with just one click.", "description": "Shorten long URLs with just one click.",
"icons": { "icons": {
"16": "assets/favicon-16.png", "16": "assets/icons/favicon-16.png",
"32": "assets/favicon-32.png", "32": "assets/icons/favicon-32.png",
"48": "assets/favicon-48.png", "48": "assets/icons/favicon-48.png",
"128": "assets/favicon-128.png" "128": "assets/icons/favicon-128.png"
}, },
"homepage_url": "https://github.com/thedevs-network/kutt-extension.git", "homepage_url": "https://github.com/thedevs-network/kutt-extension.git",
"__firefox__browser_specific_settings": { "__firefox__browser_specific_settings": {
"gecko": { "gecko": {
"id": "support@kutt.it", "id": "support@kutt.it",
"strict_min_version": "52.0" "strict_min_version": "109.0"
} }
}, },
"__chrome|firefox__author": "abhijithvijayan", "__chrome|firefox__author": "abhijithvijayan",
"__opera__developer": { "__opera__developer": {
"name": "abhijithvijayan" "name": "abhijithvijayan"
}, },
"browser_action": { "action": {
"default_popup": "popup.html", "default_popup": "Popup/popup.html",
"default_icon": { "default_icon": {
"16": "assets/favicon-16.png", "16": "assets/icons/favicon-16.png",
"32": "assets/favicon-32.png", "32": "assets/icons/favicon-32.png",
"48": "assets/favicon-48.png", "48": "assets/icons/favicon-48.png",
"128": "assets/favicon-128.png" "128": "assets/icons/favicon-128.png"
}, },
"default_title": "Shorten this URL", "default_title": "Shorten this URL"
"__chrome|opera__chrome_style": false,
"__firefox__browser_style": false
}, },
"background": { "background": {
"__chrome|opera__persistent": false, "__chrome|opera__service_worker": "assets/js/background.bundle.js",
"scripts": [ "__chrome|opera__type": "module",
"js/background.bundle.js" "__firefox__scripts": ["assets/js/background.bundle.js"],
] "__firefox__type": "module"
}, },
"__chrome__minimum_chrome_version": "49", "__chrome__minimum_chrome_version": "88",
"__opera__minimum_opera_version": "36", "__opera__minimum_opera_version": "74",
"__chrome|opera__permissions": [ "permissions": [
"activeTab", "activeTab",
"storage", "storage"
"clipboardRead", ],
"host_permissions": [
"http://*/*", "http://*/*",
"https://*/*" "https://*/*"
], ],
"__firefox__permissions": [ "content_security_policy": {
"activeTab", "extension_pages": "script-src 'self'; object-src 'self';"
"storage", },
"clipboardWrite", "__chrome|opera__options_page": "Options/options.html",
"clipboardRead",
"http://*/*",
"https://*/*"
],
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"__chrome|opera__options_page": "options.html",
"options_ui": { "options_ui": {
"page": "options.html", "page": "Options/options.html",
"open_in_tab": true, "open_in_tab": true
"__chrome__chrome_style": false,
"__firefox|opera__browser_style": false
} }
} }

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 984 B

After

Width:  |  Height:  |  Size: 984 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -1,3 +1,5 @@
@use 'variables' as *;
.icon { .icon {
width: 26px; width: 26px;
height: 26px; height: 26px;
@@ -14,32 +16,10 @@
border-radius: 100%; border-radius: 100%;
svg { svg {
// width: 100%;
// height: 100%;
transition: all 0.2s ease-out 0s; transition: all 0.2s ease-out 0s;
} }
} }
.max-w-min { .d-none {
max-width: min-content; display: none !important;
}
.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;
} }

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

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

View File

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

View File

@@ -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%;
}

View File

@@ -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%);

View File

@@ -1,6 +1,3 @@
@import '~tailwindcss/dist/base.min.css'; @use 'reset';
@use 'variables';
@import "base/fonts"; @use 'components';
@import "base/variables";
@import "base/components";
@import "base/reset";

View File

@@ -1,4 +1,4 @@
import {browser} from 'webextension-polyfill-ts'; import browser from 'webextension-polyfill';
const messageUtil = { const messageUtil = {
send(name: string, params?: unknown): Promise<any> { send(name: string, params?: unknown): Promise<any> {

View File

@@ -1,4 +1,4 @@
import {browser} from 'webextension-polyfill-ts'; import browser from 'webextension-polyfill';
import {DomainEntryProperties} from '../Background'; import {DomainEntryProperties} from '../Background';

View File

@@ -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> { export function openExtOptionsPage(): Promise<void> {
return browser.runtime.openOptionsPage(); return browser.runtime.openOptionsPage();
@@ -7,7 +8,7 @@ export function openExtOptionsPage(): Promise<void> {
export function openHistoryPage(): Promise<Tabs.Tab> { export function openHistoryPage(): Promise<Tabs.Tab> {
return browser.tabs.create({ return browser.tabs.create({
active: true, active: true,
url: 'history.html', url: 'History/history.html',
}); });
} }

View File

@@ -1,8 +0,0 @@
module.exports = {
purge: [],
theme: {
extend: {},
},
variants: {},
plugins: [],
}

View File

@@ -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
}
]
]
}

View File

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

View File

@@ -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
}
}
}

View File

@@ -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
View File

@@ -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.*

View File

@@ -1 +0,0 @@
v18.18.0

View File

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

View File

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

View File

@@ -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.

View File

@@ -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
| [![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png)](/) | [![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png)](/) | [![Opera](https://raw.github.com/alrra/browser-logos/master/src/opera/opera_48x48.png)](/) | [![Edge](https://raw.github.com/alrra/browser-logos/master/src/edge/edge_48x48.png)](/) | [![Yandex](https://raw.github.com/alrra/browser-logos/master/src/yandex/yandex_48x48.png)](/) | [![Brave](https://raw.github.com/alrra/browser-logos/master/src/brave/brave_48x48.png)](/) | [![vivaldi](https://raw.github.com/alrra/browser-logos/master/src/vivaldi/vivaldi_48x48.png)](/) |
| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| 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 extensions 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -1,5 +0,0 @@
import {browser} from 'webextension-polyfill-ts';
browser.runtime.onInstalled.addListener((): void => {
console.log('🦄', 'extension installed');
});

View File

@@ -1,3 +0,0 @@
console.log('helloworld from content script');
export {};

View File

@@ -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;

View File

@@ -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'));

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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'));

View File

@@ -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;
}
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -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"
]
}]
}

View File

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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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"
]
}

View File

@@ -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}},
},
],
},
},
}),
],
},
};

View File

@@ -1,23 +1,15 @@
{ {
"extends": "@abhijithvijayan/tsconfig", "extends": "@abhijithvijayan/tsconfig",
"compilerOptions": { "compilerOptions": {
"target": "es5", // ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. "target": "ESNext",
"module": "esnext", // Module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. "module": "ESNext",
"lib": [ "noEmit": true,
"dom", "lib": ["dom", "dom.iterable", "ES2023"],
"dom.iterable", "moduleResolution": "bundler",
"esnext"
],
"declaration": false,
"isolatedModules": true, "isolatedModules": true,
/* Additional Checks */ "declaration": false,
"useDefineForClassFields": true, "jsx": "react-jsx",
"skipLibCheck": true, "skipLibCheck": true
}, },
"include": [ "include": ["source"]
"source",
"twin.d.ts",
"cssprop.d.ts",
"webpack.config.js"
]
} }

7
twin.d.ts vendored
View File

@@ -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;
}

View File

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

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