├── public
├── favicon.ico
├── icons
│ ├── maskable_icon.png
│ ├── maskable_icon_x128.png
│ ├── maskable_icon_x192.png
│ ├── maskable_icon_x384.png
│ ├── maskable_icon_x48.png
│ ├── maskable_icon_x512.png
│ ├── maskable_icon_x72.png
│ └── maskable_icon_x96.png
├── screenshots
│ ├── iphone.png
│ └── ipad-pro.png
├── robots.txt
├── images
│ ├── arrow-right-solid.svg
│ ├── exchange-alt-solid.svg
│ ├── tools-solid.svg
│ ├── undraw_at_home_octe.svg
│ ├── undraw_convert_2gjv.svg
│ ├── undraw_Live_collaboration_re_60ha.svg
│ └── undraw_authentication_fsn5.svg
├── sitemap.xml
├── manifest.json
├── sw.js
└── workbox-ea903bce.js
├── .prettierrc.json
├── jsconfig.json
├── styles
├── globals.css
└── theme.js
├── next.config.js
├── components
├── WordCounters.js
├── Nav.js
├── ColorChanger.js
├── Layout.js
├── SectionItem.js
├── Footer.js
└── Header.js
├── .prettierignore
├── .gitignore
├── pages
├── _app.js
├── index.js
├── _document.js
├── word-count.js
├── secure-random-password-generator.js
└── px-converter.js
├── .eslintignore
├── .eslintrc.yml
├── next-seo-config.js
├── lib
└── constants.js
├── scripts
└── generate-sitemap.js
├── README.md
├── package.json
└── hooks
├── useWordCount.js
├── usePasswordGenerator.js
└── usePxConverter.js
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dastasoft/handy-tools/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "arrowParens": "avoid",
4 | "singleQuote": true
5 | }
6 |
--------------------------------------------------------------------------------
/public/icons/maskable_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dastasoft/handy-tools/HEAD/public/icons/maskable_icon.png
--------------------------------------------------------------------------------
/public/screenshots/iphone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dastasoft/handy-tools/HEAD/public/screenshots/iphone.png
--------------------------------------------------------------------------------
/public/screenshots/ipad-pro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dastasoft/handy-tools/HEAD/public/screenshots/ipad-pro.png
--------------------------------------------------------------------------------
/public/icons/maskable_icon_x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dastasoft/handy-tools/HEAD/public/icons/maskable_icon_x128.png
--------------------------------------------------------------------------------
/public/icons/maskable_icon_x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dastasoft/handy-tools/HEAD/public/icons/maskable_icon_x192.png
--------------------------------------------------------------------------------
/public/icons/maskable_icon_x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dastasoft/handy-tools/HEAD/public/icons/maskable_icon_x384.png
--------------------------------------------------------------------------------
/public/icons/maskable_icon_x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dastasoft/handy-tools/HEAD/public/icons/maskable_icon_x48.png
--------------------------------------------------------------------------------
/public/icons/maskable_icon_x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dastasoft/handy-tools/HEAD/public/icons/maskable_icon_x512.png
--------------------------------------------------------------------------------
/public/icons/maskable_icon_x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dastasoft/handy-tools/HEAD/public/icons/maskable_icon_x72.png
--------------------------------------------------------------------------------
/public/icons/maskable_icon_x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dastasoft/handy-tools/HEAD/public/icons/maskable_icon_x96.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /api
3 | Host: https://handy-tools.dastasoft.com/
4 | Sitemap: https://handy-tools.dastasoft.com/sitemap.xml
5 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/components/*": ["components/*"],
6 | "@/hooks/*": ["hooks/*"],
7 | "@/lib/*": ["lib/*"],
8 | "@/styles/*": ["styles/*"]
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | height: 100%;
6 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
7 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
8 | }
9 |
10 | #__next {
11 | height: 100%;
12 | }
13 |
14 | a {
15 | color: inherit;
16 | text-decoration: none;
17 | }
18 |
19 | * {
20 | box-sizing: border-box;
21 | }
22 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withPWA = require('next-pwa')
2 | const runtimeCaching = require('next-pwa/cache')
3 |
4 | module.exports = withPWA({
5 | pwa: {
6 | dest: 'public',
7 | runtimeCaching,
8 | },
9 | future: {
10 | webpack5: true,
11 | },
12 | webpack: (config, { isServer }) => {
13 | if (isServer) {
14 | require('./scripts/generate-sitemap')
15 | }
16 |
17 | return config
18 | },
19 | })
20 |
--------------------------------------------------------------------------------
/styles/theme.js:
--------------------------------------------------------------------------------
1 | import { extendTheme } from '@chakra-ui/react'
2 | import { mode } from '@chakra-ui/theme-tools'
3 |
4 | const theme = extendTheme({
5 | fonts: {
6 | heading: 'Lato',
7 | body: 'Roboto',
8 | },
9 | styles: {
10 | global: props => ({
11 | 'body, h2, p': {
12 | color: mode('gray.700', 'whiteAlpha.900')(props),
13 | },
14 | }),
15 | },
16 | })
17 |
18 | export default theme
19 |
--------------------------------------------------------------------------------
/components/WordCounters.js:
--------------------------------------------------------------------------------
1 | import { Flex, Text, useColorModeValue } from '@chakra-ui/react'
2 |
3 | export default function WordCounters({ value, caption }) {
4 | const bg = useColorModeValue('whiteAlpha.900', 'gray.800')
5 |
6 | return (
7 |
8 |
9 | {value}
10 |
11 | {caption}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/public/images/arrow-right-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | https://handy-tools.dastasoft.com
5 |
6 |
7 |
8 | https://handy-tools.dastasoft.com/px-converter
9 |
10 |
11 |
12 | https://handy-tools.dastasoft.com/secure-random-password-generator
15 |
16 |
17 |
18 | https://handy-tools.dastasoft.com/word-count
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | #eslint
37 | .eslintcache
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { ChakraProvider } from '@chakra-ui/react'
2 | import { DefaultSeo } from 'next-seo'
3 |
4 | import SEO from '../next-seo-config'
5 |
6 | import Layout from '@/components/Layout'
7 |
8 | import '@/styles/globals.css'
9 | import theme from '@/styles/theme'
10 |
11 | function MyApp({ Component, pageProps }) {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | export default MyApp
23 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | public/sw.*
37 | public/workbox*
--------------------------------------------------------------------------------
/components/Nav.js:
--------------------------------------------------------------------------------
1 | import { SimpleGrid } from '@chakra-ui/react'
2 |
3 | import { links } from '@/lib/constants'
4 | import SectionItem from './SectionItem'
5 |
6 | const Nav = ({ onClose }) => {
7 | return (
8 |
9 | {links.map(link => (
10 |
17 | ))}
18 |
19 | )
20 | }
21 |
22 | export default Nav
23 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import { SimpleGrid } from '@chakra-ui/react'
2 |
3 | import SectionItem from '@/components/SectionItem'
4 | import { links } from '@/lib/constants'
5 |
6 | export default function Home() {
7 | return (
8 |
9 | {links
10 | .filter(link => link.href !== '/')
11 | .map(link => (
12 |
18 | ))}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | env:
2 | browser: true
3 | es2021: true
4 | node: true
5 | extends:
6 | - 'plugin:react/recommended'
7 | - standard
8 | - prettier
9 | parserOptions:
10 | ecmaFeatures:
11 | jsx: true
12 | ecmaVersion: 12
13 | sourceType: module
14 | plugins:
15 | - react
16 | rules:
17 | {
18 | 'react/react-in-jsx-scope': 'off',
19 | 'react/display-name': 'off',
20 | 'react/prop-types': 'off',
21 | 'react/jsx-props-no-spreading': 'off',
22 | 'jsx-a11y/href-no-hash': ['off'],
23 | 'jsx-a11y/anchor-is-valid': ['off'],
24 | 'react/jsx-filename-extension': ['warn', { extensions: ['.js', '.jsx'] }],
25 | }
26 |
--------------------------------------------------------------------------------
/public/images/exchange-alt-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/ColorChanger.js:
--------------------------------------------------------------------------------
1 | import { IconButton, useColorMode, useColorModeValue } from '@chakra-ui/react'
2 | import { SunIcon, MoonIcon } from '@chakra-ui/icons'
3 |
4 | export default function ColorChanger() {
5 | const { toggleColorMode } = useColorMode()
6 | const text = useColorModeValue('dark', 'light')
7 | const SwitchIcon = useColorModeValue(MoonIcon, SunIcon)
8 |
9 | return (
10 | }
18 | />
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/next-seo-config.js:
--------------------------------------------------------------------------------
1 | import {
2 | AUTHOR,
3 | DESCRIPTION,
4 | HOMEPAGE,
5 | THUMBNAIL,
6 | TITLE,
7 | } from '@/lib/constants'
8 |
9 | export default {
10 | title: TITLE,
11 | description: DESCRIPTION,
12 | canonical: HOMEPAGE,
13 | languageAlternates: [
14 | {
15 | hrefLang: 'en',
16 | href: HOMEPAGE,
17 | },
18 | ],
19 | openGraph: {
20 | profile: {
21 | username: AUTHOR,
22 | },
23 | type: 'website',
24 | locale: 'en',
25 | url: HOMEPAGE,
26 | site_name: TITLE,
27 | images: [
28 | {
29 | url: THUMBNAIL,
30 | width: 200,
31 | height: 200,
32 | alt: TITLE,
33 | },
34 | ],
35 | },
36 | }
37 |
--------------------------------------------------------------------------------
/components/Layout.js:
--------------------------------------------------------------------------------
1 | import { Box, Flex, useColorModeValue } from '@chakra-ui/react'
2 |
3 | import Header from '@/components/Header'
4 | import Footer from '@/components/Footer'
5 |
6 | const Layout = ({ children }) => {
7 | const bg = useColorModeValue('linkedin.100', 'gray.700')
8 |
9 | return (
10 |
11 |
12 |
20 | {children}
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export default Layout
28 |
--------------------------------------------------------------------------------
/public/images/tools-solid.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import { ColorModeScript } from '@chakra-ui/react'
2 | import NextDocument, { Html, Head, Main, NextScript } from 'next/document'
3 |
4 | import theme from '@/styles/theme'
5 |
6 | export default class Document extends NextDocument {
7 | render() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/components/SectionItem.js:
--------------------------------------------------------------------------------
1 | import { Box, Link, Heading, Image, Flex } from '@chakra-ui/react'
2 | import NextLink from 'next/link'
3 |
4 | export default function SectionItem({ img, title, link, onClick }) {
5 | return (
6 |
7 |
8 |
20 |
21 |
22 |
23 |
24 | {title}
25 |
26 |
27 |
28 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/lib/constants.js:
--------------------------------------------------------------------------------
1 | export const AUTHOR = 'dastasoft'
2 | export const DESCRIPTION = 'A collection of web tools to make your life easier'
3 | export const HOMEPAGE = 'https://handy-tools.dastasoft.com/'
4 | export const THUMBNAIL =
5 | 'https://media.discordapp.net/attachments/834717775057911898/837286075310407720/i-clipart-tool-10.png'
6 | export const TITLE = 'Handy Tools | dastasoft'
7 | export const links = [
8 | {
9 | title: 'Home',
10 | href: '/',
11 | thumbnail: '/images/undraw_at_home_octe.svg',
12 | },
13 | {
14 | title: 'Secure Password Generator',
15 | href: '/secure-random-password-generator',
16 | thumbnail: '/images/undraw_authentication_fsn5.svg',
17 | },
18 | {
19 | title: 'Word Count',
20 | href: '/word-count',
21 | thumbnail: '/images/undraw_Live_collaboration_re_60ha.svg',
22 | },
23 | {
24 | title: 'px Converter',
25 | href: '/px-converter',
26 | thumbnail: '/images/undraw_convert_2gjv.svg',
27 | },
28 | ]
29 |
--------------------------------------------------------------------------------
/scripts/generate-sitemap.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const globby = require('globby')
3 | const prettier = require('prettier')
4 |
5 | ;(async () => {
6 | const prettierConfig = await prettier.resolveConfig('./.prettierrc.js')
7 |
8 | const pages = await globby([
9 | 'pages/**/*{.js,.mdx}',
10 | '_posts/*{.md,.mdx}',
11 | '!pages/_*.js',
12 | '!pages/api',
13 | '!pages/**/[slug].js',
14 | ])
15 | const sitemap = `
16 |
17 |
18 | ${pages
19 | .map(page => {
20 | const path = page
21 | .replace('pages', '')
22 | .replace('_posts', '/posts')
23 | .replace('.js', '')
24 | .replace('.md', '')
25 | .replace('.mdx', '')
26 | const route = path === '/index' ? '' : path
27 |
28 | return `
29 |
30 | ${`https://handy-tools.dastasoft.com${route}`}
31 |
32 | `
33 | })
34 | .join('')}
35 |
36 | `
37 |
38 | const formatted = prettier.format(sitemap, {
39 | ...prettierConfig,
40 | parser: 'html',
41 | })
42 |
43 | fs.writeFileSync('public/sitemap.xml', formatted)
44 | })()
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Handy Tools
2 |
3 | A collection of web tools to make your life easier. Made with Next.js and Chakra UI. You can visit a [live site](https://handy-tools.dastasoft.com)
4 |
5 | Project made with Next.js and Chakra UI to explain the Chakra UI component library in [my tech blog](https://blog.dastasoft.com/posts/why-you-should-use-chakra-ui-in-react).
6 |
7 | ## Support Me
8 |
9 | Thank you for using my project! If you find it helpful, please consider supporting me in one of the following ways:
10 |
11 | - Star the project: If you haven't done so already, please consider giving this project a star on GitHub. It's a quick and easy way to show your appreciation and help others discover the project.
12 | - Spread the word: If you know someone who might benefit from this project, please share it with them! Whether it's on social media, in a blog post, or in a conversation, every bit helps.
13 | - Donate: If you would like to support the project financially, you can donate via this button. Your contribution will be used to develop new articles and projects.
14 |
15 |
16 |
17 | Thank you again for using my project, and I appreciate any support you can offer!
18 |
19 | ## Screenshots
20 |
21 | 
22 | 
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "handy-tools",
3 | "version": "0.0.1",
4 | "private": false,
5 | "author": "dastasoft (https://dastasoft.com)",
6 | "license": "MIT",
7 | "description": "",
8 | "scripts": {
9 | "build": "next build",
10 | "deploy": "next build && next export",
11 | "dev": "next dev",
12 | "export": "next export",
13 | "lint": "eslint . --fix",
14 | "start": "next start"
15 | },
16 | "dependencies": {
17 | "@chakra-ui/icons": "^1.0.12",
18 | "@chakra-ui/react": "^1.6.0",
19 | "@emotion/react": "^11",
20 | "@emotion/styled": "^11.3.0",
21 | "@fontsource/lato": "^4.2.2",
22 | "@fontsource/roboto": "^4.2.3",
23 | "framer-motion": "^4.1.11",
24 | "next": "10.2.0",
25 | "next-pwa": "^5.2.15",
26 | "next-seo": "^4.24.0",
27 | "react": "17.0.2",
28 | "react-dom": "17.0.2",
29 | "react-icons": "^4.2.0"
30 | },
31 | "devDependencies": {
32 | "eslint": "^7.25.0",
33 | "eslint-config-prettier": "^8.3.0",
34 | "eslint-config-standard": "^16.0.2",
35 | "eslint-plugin-import": "^2.22.1",
36 | "eslint-plugin-node": "^11.1.0",
37 | "eslint-plugin-promise": "^5.1.0",
38 | "eslint-plugin-react": "^7.23.2",
39 | "lint-staged": ">=10",
40 | "prettier": "^2.2.1",
41 | "simple-git-hooks": ">=2.4.1"
42 | },
43 | "simple-git-hooks": {
44 | "pre-commit": "npx lint-staged"
45 | },
46 | "lint-staged": {
47 | "*.js": "eslint --cache --fix",
48 | "*.{js,css,md}": "prettier --write"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/components/Footer.js:
--------------------------------------------------------------------------------
1 | import { Link, Icon, Flex, Text, Stack } from '@chakra-ui/react'
2 | import { IoLogoLinkedin } from 'react-icons/io'
3 | import { MdEmail } from 'react-icons/md'
4 | import { DiGithubBadge } from 'react-icons/di'
5 | import { FaDev } from 'react-icons/fa'
6 |
7 | const links = [
8 | {
9 | icon: FaDev,
10 | label: 'dev.to',
11 | href: 'https://dev.to/dastasoft',
12 | },
13 | {
14 | icon: DiGithubBadge,
15 | label: 'GitHub',
16 | href: 'https://github.com/dastasoft',
17 | },
18 | {
19 | icon: IoLogoLinkedin,
20 | label: 'LinkedIn',
21 | href: 'https://www.linkedin.com/in/dastasoft/',
22 | },
23 | {
24 | icon: MdEmail,
25 | label: 'Email',
26 | href: 'mailto:dastasoft@protonmail.com',
27 | },
28 | ]
29 |
30 | const FooterLink = ({ icon, href, label }) => (
31 |
32 |
33 |
34 | )
35 |
36 | const Footer = ({ h }) => {
37 | return (
38 |
45 |
46 | Made by{' '}
47 |
53 | dastasoft
54 |
55 |
56 |
57 | {links.map(link => (
58 |
59 | ))}
60 |
61 |
62 | )
63 | }
64 |
65 | export default Footer
66 |
--------------------------------------------------------------------------------
/hooks/useWordCount.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | function countCharacters(text) {
4 | return text.length
5 | }
6 |
7 | function countWords(text) {
8 | return text.split(' ').length
9 | }
10 |
11 | function countSentences(text) {
12 | return text.split('.').length
13 | }
14 |
15 | function countParagraphs(text) {
16 | return text.split('.\n').length
17 | }
18 |
19 | function getMostRepeatedWord(text) {
20 | const words = countRepeatedWords(text)
21 |
22 | return Object.keys(words)
23 | .map(word => ({ word, count: words[word] }))
24 | .filter(word => word.count > 1)
25 | .sort((a, b) => b.count - a.count)
26 | }
27 |
28 | function countRepeatedWords(text) {
29 | const words = text.split(' ')
30 | const repeatedWords = words.reduce((acc, curr) => {
31 | const re = new RegExp(curr, 'g')
32 | if (!acc[curr] && curr.match(/[a-zA-z]/)) acc[curr] = text.match(re).length
33 | return acc
34 | }, {})
35 |
36 | return repeatedWords
37 | }
38 |
39 | export default function useWordCount() {
40 | const [text, setText] = useState('')
41 | const [characterCount, setCharacterCount] = useState(0)
42 | const [wordCount, setWordCount] = useState(0)
43 | const [sentenceCount, setSentencetCount] = useState(0)
44 | const [paragraphCount, setPragraphCount] = useState(0)
45 | const [mostUsedWord, setMostUsedWord] = useState([])
46 |
47 | const handleTextChange = newValue => {
48 | if (newValue !== text) {
49 | setText(newValue)
50 | setCharacterCount(countCharacters(newValue))
51 | setWordCount(countWords(newValue))
52 | setSentencetCount(countSentences(newValue))
53 | setPragraphCount(countParagraphs(newValue))
54 | setMostUsedWord(getMostRepeatedWord(newValue))
55 | }
56 | }
57 |
58 | return {
59 | text,
60 | characterCount,
61 | wordCount,
62 | sentenceCount,
63 | paragraphCount,
64 | mostUsedWord,
65 | handleTextChange,
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/pages/word-count.js:
--------------------------------------------------------------------------------
1 | import {
2 | SimpleGrid,
3 | Flex,
4 | Text,
5 | Heading,
6 | Textarea,
7 | Box,
8 | useColorModeValue,
9 | } from '@chakra-ui/react'
10 |
11 | import WordCounter from '@/components/WordCounters'
12 | import useWordCount from '@/hooks/useWordCount'
13 |
14 | const WordCount = () => {
15 | const {
16 | text,
17 | characterCount,
18 | wordCount,
19 | sentenceCount,
20 | paragraphCount,
21 | mostUsedWord,
22 | handleTextChange,
23 | } = useWordCount()
24 |
25 | const bg = useColorModeValue('whiteAlpha.900', 'gray.800')
26 |
27 | return (
28 |
29 | Word Count
30 |
38 |
39 |
40 |
41 |
42 |
43 |
70 | )
71 | }
72 |
73 | export default WordCount
74 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "background_color": "#6da34d",
3 | "description": "A collection of web tools to make your life easier",
4 | "dir": null,
5 | "display": "fullscreen",
6 | "lang": "English",
7 | "name": "Handy Tools | dastasoft",
8 | "orientation": "portrait-primary",
9 | "prefer_related_applications": null,
10 | "related_applications": null,
11 | "scope": "/",
12 | "short_name": "Handy Tools | dastasoft",
13 | "start_url": "/",
14 | "theme_color": "#ffffff",
15 | "categories": ["productivity", "utilities"],
16 | "screenshots": [
17 | {
18 | "src": "screenshots/blog1.png",
19 | "sizes": "1280x800",
20 | "type": "image/png"
21 | },
22 | {
23 | "src": "screenshots/blog2.png",
24 | "sizes": "750x1334",
25 | "type": "image/png"
26 | }
27 | ],
28 | "iarc_rating_id": null,
29 | "icons": [
30 | {
31 | "src": "icons/maskable_icon_x48.png",
32 | "sizes": "48x48",
33 | "type": "image/png",
34 | "purpose": "any maskable"
35 | },
36 | {
37 | "src": "icons/maskable_icon_x72.png",
38 | "sizes": "72x72",
39 | "type": "image/png",
40 | "purpose": "any maskable"
41 | },
42 | {
43 | "src": "icons/maskable_icon_x96.png",
44 | "sizes": "96x96",
45 | "type": "image/png",
46 | "purpose": "any maskable"
47 | },
48 | {
49 | "src": "icons/maskable_icon_x128.png",
50 | "sizes": "128x128",
51 | "type": "image/png",
52 | "purpose": "any maskable"
53 | },
54 | {
55 | "src": "icons/maskable_icon_x192.png",
56 | "sizes": "192x192",
57 | "type": "image/png",
58 | "purpose": "any maskable"
59 | },
60 | {
61 | "src": "icons/maskable_icon_x384.png",
62 | "sizes": "384x384",
63 | "type": "image/png",
64 | "purpose": "any maskable"
65 | },
66 | {
67 | "src": "icons/maskable_icon_x512.png",
68 | "sizes": "512x512",
69 | "type": "image/png",
70 | "purpose": "any maskable"
71 | }
72 | ],
73 | "url": "https://handy-tools.dastasoft.com/"
74 | }
75 |
--------------------------------------------------------------------------------
/hooks/usePasswordGenerator.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | const PATTERNS = {
4 | uppercase: /[A-Z]/,
5 | lowercase: /[a-z]/,
6 | numerical: /[0-9]/,
7 | symbols: /[!@#$%^&*]/,
8 | }
9 |
10 | export default function usePasswordGenerator() {
11 | const MIN_PASSWORD_LENGHT = 5
12 | const MAX_PASSWORD_LENGTH = 128
13 | const [password, setPassword] = useState('')
14 | const [length, setLenght] = useState(MAX_PASSWORD_LENGTH)
15 | const [uppercase, setUppercase] = useState(true)
16 | const [lowercase, setLowercase] = useState(true)
17 | const [numerical, setNumerical] = useState(true)
18 | const [symbols, setSymbols] = useState(true)
19 |
20 | const checkPattern = value => {
21 | if (uppercase && PATTERNS.uppercase.test(value)) return true
22 |
23 | if (lowercase && PATTERNS.lowercase.test(value)) return true
24 |
25 | if (numerical && PATTERNS.numerical.test(value)) return true
26 |
27 | if (symbols && PATTERNS.symbols.test(value)) return true
28 | }
29 |
30 | const getRandomByte = () => {
31 | if (window.crypto?.getRandomValues) {
32 | const result = new Uint8Array(1)
33 | window.crypto.getRandomValues(result)
34 | return result[0]
35 | } else if (window.msCrypto?.getRandomValues) {
36 | const result = new Uint8Array(1)
37 | window.msCrypto.getRandomValues(result)
38 | return result[0]
39 | } else {
40 | return Math.floor(Math.random() * 256)
41 | }
42 | }
43 |
44 | const generatePassword = () => {
45 | if (uppercase || lowercase || numerical || symbols) {
46 | setPassword(
47 | Array.apply(null, { length: length })
48 | .map(() => {
49 | let result
50 |
51 | do {
52 | result = String.fromCharCode(getRandomByte())
53 | } while (!checkPattern(result))
54 |
55 | return result
56 | })
57 | .join('')
58 | )
59 | } else {
60 | setPassword('You must select at least one option')
61 | }
62 | }
63 |
64 | return {
65 | MIN_PASSWORD_LENGHT,
66 | MAX_PASSWORD_LENGTH,
67 | password,
68 | length,
69 | setLenght,
70 | uppercase,
71 | setUppercase,
72 | lowercase,
73 | setLowercase,
74 | numerical,
75 | setNumerical,
76 | symbols,
77 | setSymbols,
78 | generatePassword,
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/hooks/usePxConverter.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | export default function usePxConverter() {
4 | const [option, setOption] = useState('PX_REM')
5 | const [px, setPx] = useState(0)
6 | const [rem, setRem] = useState(0)
7 | const [em, setEm] = useState(0)
8 | const [fontBase, setFontBase] = useState(16)
9 |
10 | const options = Object.freeze({
11 | PX_REM: 'PX to REM',
12 | PX_EM: 'PX to EM',
13 | REM_PX: 'REM to PX',
14 | EM_PX: 'EM to PX',
15 | })
16 |
17 | const optionChange = newOption => {
18 | const newRelativeValue = px / fontBase
19 | setOption(newOption)
20 |
21 | if (newOption === 'PX_REM') {
22 | setRem(newRelativeValue)
23 | } else if (newOption === 'PX_EM') {
24 | setEm(newRelativeValue)
25 | }
26 | }
27 |
28 | const swapInputs = () => {
29 | let newOption = ''
30 |
31 | if (option === 'PX_REM') {
32 | newOption = 'REM_PX'
33 | } else if (option === 'PX_EM') {
34 | newOption = 'EM_PX'
35 | } else if (option === 'REM_PX') {
36 | newOption = 'PX_REM'
37 | } else {
38 | newOption = 'PX_EM'
39 | }
40 |
41 | optionChange(newOption)
42 | }
43 |
44 | const onOptionChange = event => {
45 | optionChange(event.target.value)
46 | }
47 |
48 | const onPxChange = event => {
49 | const { value: newPxValue } = event.target
50 | const newRelativeValue = newPxValue / fontBase
51 |
52 | setPx(newPxValue)
53 |
54 | if (option === 'PX_REM' || option === 'REM_PX') {
55 | setRem(newRelativeValue)
56 | } else if (option === 'PX_EM' || option === 'EM_PX') {
57 | setEm(newRelativeValue)
58 | }
59 | }
60 |
61 | const onRemChange = event => {
62 | const { value: newRemValue } = event.target
63 |
64 | setRem(newRemValue)
65 | setPx(newRemValue * fontBase)
66 | }
67 |
68 | const onEmChange = event => {
69 | const { value: newEmValue } = event.target
70 |
71 | setEm(newEmValue)
72 | setPx(newEmValue * fontBase)
73 | }
74 |
75 | const onFontBaseChange = newFontBase => {
76 | const newRelativeValue = px / newFontBase
77 |
78 | setFontBase(newFontBase)
79 |
80 | if (option === 'PX_REM' || option === 'PX_EM') {
81 | setRem(newRelativeValue)
82 | setEm(newRelativeValue)
83 | }
84 | }
85 |
86 | return {
87 | em,
88 | fontBase,
89 | onEmChange,
90 | onFontBaseChange,
91 | onOptionChange,
92 | onPxChange,
93 | onRemChange,
94 | option,
95 | options,
96 | px,
97 | rem,
98 | swapInputs,
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/components/Header.js:
--------------------------------------------------------------------------------
1 | import {
2 | Flex,
3 | Link,
4 | Heading,
5 | useDisclosure,
6 | Button,
7 | Drawer,
8 | DrawerBody,
9 | DrawerHeader,
10 | DrawerOverlay,
11 | DrawerContent,
12 | DrawerCloseButton,
13 | useColorModeValue,
14 | Box,
15 | } from '@chakra-ui/react'
16 | import { HamburgerIcon, Icon } from '@chakra-ui/icons'
17 | import NextLink from 'next/link'
18 | import { useRouter } from 'next/router'
19 |
20 | import Nav from '@/components/Nav'
21 | import ColorChanger from '@/components/ColorChanger'
22 |
23 | const Header = ({ h }) => {
24 | const { isOpen, onOpen, onClose } = useDisclosure()
25 | const router = useRouter()
26 | const bg = useColorModeValue('linkedin.700', 'linkedin.900')
27 | const gradient = useColorModeValue(
28 | 'linkedin.300, linkedin.900',
29 | 'linkedin.900, linkedin.300'
30 | )
31 |
32 | return (
33 |
40 |
41 |
42 |
48 |
52 |
53 | Handy Tools
54 |
55 |
56 |
57 |
58 |
59 | {router.route !== '/' && (
60 | <>
61 |
64 |
65 |
66 |
67 |
68 | Navigation
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | >
77 | )}
78 |
79 | )
80 | }
81 |
82 | export default Header
83 |
--------------------------------------------------------------------------------
/public/images/undraw_at_home_octe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/undraw_convert_2gjv.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pages/secure-random-password-generator.js:
--------------------------------------------------------------------------------
1 | import {
2 | Checkbox,
3 | Box,
4 | Flex,
5 | Text,
6 | Heading,
7 | Button,
8 | Slider,
9 | SliderTrack,
10 | SliderFilledTrack,
11 | SliderThumb,
12 | useColorModeValue,
13 | } from '@chakra-ui/react'
14 |
15 | import usePasswordGenerator from '@/hooks/usePasswordGenerator'
16 |
17 | const SecureRandomPasswordGenerator = () => {
18 | const {
19 | MIN_PASSWORD_LENGHT,
20 | MAX_PASSWORD_LENGTH,
21 | password,
22 | length,
23 | setLenght,
24 | uppercase,
25 | setUppercase,
26 | lowercase,
27 | setLowercase,
28 | numerical,
29 | setNumerical,
30 | symbols,
31 | setSymbols,
32 | generatePassword,
33 | } = usePasswordGenerator()
34 | const bg = useColorModeValue('whiteAlpha.900', 'gray.800')
35 |
36 | const onCopy = () => {
37 | navigator.clipboard.writeText(password)
38 | }
39 |
40 | const onRegenerate = () => {
41 | generatePassword()
42 | }
43 |
44 | const onSlide = value => {
45 | if (value !== length) {
46 | setLenght(value)
47 | generatePassword()
48 | }
49 | }
50 |
51 | return (
52 |
60 |
61 | Password Generator
62 |
63 |
64 |
73 | {password}
74 |
75 |
76 |
79 |
82 |
83 |
84 |
85 |
86 | Length
87 |
88 | {length}
89 |
90 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | A-Z
108 | setUppercase(e.target.checked)}
113 | />
114 |
115 |
116 | a-z
117 | setLowercase(e.target.checked)}
122 | />
123 |
124 |
125 | 0-9
126 | setNumerical(e.target.checked)}
131 | />
132 |
133 |
134 | !@#$%^&*
135 | setSymbols(e.target.checked)}
140 | />
141 |
142 |
143 |
144 | )
145 | }
146 |
147 | export default SecureRandomPasswordGenerator
148 |
--------------------------------------------------------------------------------
/pages/px-converter.js:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Select,
4 | Input,
5 | Flex,
6 | Text,
7 | Heading,
8 | Editable,
9 | EditableInput,
10 | EditablePreview,
11 | useEditableControls,
12 | ButtonGroup,
13 | IconButton,
14 | Grid,
15 | GridItem,
16 | useColorModeValue,
17 | } from '@chakra-ui/react'
18 | import { CheckIcon, CloseIcon, EditIcon } from '@chakra-ui/icons'
19 | import NextImage from 'next/image'
20 |
21 | import usePxConverter from '@/hooks/usePxConverter'
22 |
23 | function EditableControls() {
24 | const {
25 | isEditing,
26 | getSubmitButtonProps,
27 | getCancelButtonProps,
28 | getEditButtonProps,
29 | } = useEditableControls()
30 |
31 | return isEditing ? (
32 |
33 | } {...getSubmitButtonProps()} />
34 | } {...getCancelButtonProps()} />
35 |
36 | ) : (
37 |
38 | } {...getEditButtonProps()} />
39 |
40 | )
41 | }
42 |
43 | export default function PxConverter() {
44 | const {
45 | em,
46 | fontBase,
47 | onEmChange,
48 | onFontBaseChange,
49 | onOptionChange,
50 | onPxChange,
51 | onRemChange,
52 | option,
53 | options,
54 | px,
55 | rem,
56 | swapInputs,
57 | } = usePxConverter()
58 | const bg = useColorModeValue('whiteAlpha.900', 'gray.800')
59 | const color = useColorModeValue('gray.800', 'whiteAlpha.900')
60 |
61 | return (
62 |
63 | Px Converter
64 |
75 |
83 |
84 |
91 | PX
92 |
93 |
94 |
95 |
100 |
101 |
102 | {(option === 'PX_REM' || option === 'REM_PX') && (
103 |
104 |
111 | REM
112 |
113 | )}
114 | {(option === 'PX_EM' || option === 'EM_PX') && (
115 |
116 |
123 | EM
124 |
125 | )}
126 |
127 |
131 |
132 | Using a {option === 'PX_EM' ? 'parent' : 'root'} font-base of{' '}
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | px
142 |
143 |
144 | )
145 | }
146 |
147 | const styles = {
148 | heading: {
149 | marginBottom: '0.5em',
150 | },
151 | flex: {
152 | alignItems: 'center',
153 | marginLeft: '0.2em',
154 | },
155 | flexCenter: {
156 | justifyContent: 'center',
157 | alignItems: 'center',
158 | },
159 | input: (bg, color) => ({
160 | bg,
161 | color,
162 | marginRight: '0.5em',
163 | }),
164 | select: (bg, color) => ({
165 | bg,
166 | color,
167 | maxWidth: '400px',
168 | margin: '0 auto',
169 | }),
170 | grid: {
171 | maxWidth: '600px',
172 | margin: '1.5em auto',
173 | },
174 | gridItem: {
175 | display: 'flex',
176 | alignItems: 'center',
177 | marginX: '1em',
178 | },
179 | buttonGroup: {
180 | justifyContent: 'center',
181 | size: 'sm',
182 | marginX: '0.5em',
183 | },
184 | }
185 |
--------------------------------------------------------------------------------
/public/images/undraw_Live_collaboration_re_60ha.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/sw.js:
--------------------------------------------------------------------------------
1 | if (!self.define) {
2 | const e = e => {
3 | 'require' !== e && (e += '.js')
4 | let s = Promise.resolve()
5 | return (
6 | a[e] ||
7 | (s = new Promise(async s => {
8 | if ('document' in self) {
9 | const a = document.createElement('script')
10 | ;(a.src = e), document.head.appendChild(a), (a.onload = s)
11 | } else importScripts(e), s()
12 | })),
13 | s.then(() => {
14 | if (!a[e]) throw new Error(`Module ${e} didn’t register its module`)
15 | return a[e]
16 | })
17 | )
18 | },
19 | s = (s, a) => {
20 | Promise.all(s.map(e)).then(e => a(1 === e.length ? e[0] : e))
21 | },
22 | a = { require: Promise.resolve(s) }
23 | self.define = (s, i, n) => {
24 | a[s] ||
25 | (a[s] = Promise.resolve().then(() => {
26 | let a = {}
27 | const c = { uri: location.origin + s.slice(1) }
28 | return Promise.all(
29 | i.map(s => {
30 | switch (s) {
31 | case 'exports':
32 | return a
33 | case 'module':
34 | return c
35 | default:
36 | return e(s)
37 | }
38 | })
39 | ).then(e => {
40 | const s = n(...e)
41 | return a.default || (a.default = s), a
42 | })
43 | }))
44 | }
45 | }
46 | define('./sw.js', ['./workbox-ea903bce'], function (e) {
47 | 'use strict'
48 | importScripts(),
49 | self.skipWaiting(),
50 | e.clientsClaim(),
51 | e.precacheAndRoute(
52 | [
53 | {
54 | url: '/_next/static/chunks/109-e03b9801ea264ef90897.js',
55 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
56 | },
57 | {
58 | url: '/_next/static/chunks/1bfc9850-2f9766d778166990fcb3.js',
59 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
60 | },
61 | {
62 | url: '/_next/static/chunks/249-3caa65fa1240cb8415a8.js',
63 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
64 | },
65 | {
66 | url: '/_next/static/chunks/252f366e-715e994444de56cbbf92.js',
67 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
68 | },
69 | {
70 | url: '/_next/static/chunks/27-d0ea4fe3b445566e0496.js',
71 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
72 | },
73 | {
74 | url: '/_next/static/chunks/297-ba7da716e0e05e5a4444.js',
75 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
76 | },
77 | {
78 | url: '/_next/static/chunks/433-b948c39ed2770f6a764c.js',
79 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
80 | },
81 | {
82 | url: '/_next/static/chunks/461-27285d7dd180cf2c390c.js',
83 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
84 | },
85 | {
86 | url: '/_next/static/chunks/572-6700e76f1121206ba6f4.js',
87 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
88 | },
89 | {
90 | url: '/_next/static/chunks/70b165ca-346ae96044dba610d799.js',
91 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
92 | },
93 | {
94 | url: '/_next/static/chunks/894-107232f0c384eea322f0.js',
95 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
96 | },
97 | {
98 | url: '/_next/static/chunks/914-95a6ee0fc5e196aa802c.js',
99 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
100 | },
101 | {
102 | url: '/_next/static/chunks/95b64a6e-e119003f2c5d71609214.js',
103 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
104 | },
105 | {
106 | url: '/_next/static/chunks/framework-e4c3747d6bc556d57d87.js',
107 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
108 | },
109 | {
110 | url: '/_next/static/chunks/main-85ca460f59960b26ad8c.js',
111 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
112 | },
113 | {
114 | url: '/_next/static/chunks/pages/_app-2c50fec8004ae6e413e9.js',
115 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
116 | },
117 | {
118 | url: '/_next/static/chunks/pages/_error-17e51c17671ff335d9dc.js',
119 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
120 | },
121 | {
122 | url: '/_next/static/chunks/pages/index-741d291f321c25658340.js',
123 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
124 | },
125 | {
126 | url:
127 | '/_next/static/chunks/pages/px-converter-b7f32a065c74f78a93e1.js',
128 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
129 | },
130 | {
131 | url:
132 | '/_next/static/chunks/pages/secure-random-password-generator-810e9eb93a83483582d7.js',
133 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
134 | },
135 | {
136 | url: '/_next/static/chunks/pages/word-count-5a94959c7d6a4cebc4bf.js',
137 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
138 | },
139 | {
140 | url: '/_next/static/chunks/polyfills-8683bd742a84c1edd48c.js',
141 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
142 | },
143 | {
144 | url: '/_next/static/chunks/webpack-52fad6895a24a90b56be.js',
145 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
146 | },
147 | {
148 | url: '/_next/static/css/e2bb55241b9412bb174e.css',
149 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
150 | },
151 | {
152 | url: '/_next/static/uUBZvsXwY6iJhxaqUOZG2/_buildManifest.js',
153 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
154 | },
155 | {
156 | url: '/_next/static/uUBZvsXwY6iJhxaqUOZG2/_ssgManifest.js',
157 | revision: 'uUBZvsXwY6iJhxaqUOZG2',
158 | },
159 | { url: '/favicon.ico', revision: 'b3b1781becd0606a9a856174c4b9fedd' },
160 | {
161 | url: '/icons/maskable_icon.png',
162 | revision: '8e4c0b928a59bc213c79e9234ff39d69',
163 | },
164 | {
165 | url: '/icons/maskable_icon_x128.png',
166 | revision: 'b93c4c2da66b2b9ca4ad35961a6fe17c',
167 | },
168 | {
169 | url: '/icons/maskable_icon_x192.png',
170 | revision: '52e548af5a5e02e390274b5360bf28be',
171 | },
172 | {
173 | url: '/icons/maskable_icon_x384.png',
174 | revision: '714dec57401e11bdcb3bef48a118a719',
175 | },
176 | {
177 | url: '/icons/maskable_icon_x48.png',
178 | revision: '1d3fcd66086b558778fe608cdc910847',
179 | },
180 | {
181 | url: '/icons/maskable_icon_x512.png',
182 | revision: '27462e2071519f1555f8674470003dca',
183 | },
184 | {
185 | url: '/icons/maskable_icon_x72.png',
186 | revision: '0d2c79040f34fdc9f2869f009c15695c',
187 | },
188 | {
189 | url: '/icons/maskable_icon_x96.png',
190 | revision: 'cdaca23b2a0b111a2dff35aa5606c2b6',
191 | },
192 | {
193 | url: '/images/arrow-right-solid.svg',
194 | revision: '3aa5dcbccaab655636f1682bfe2345d2',
195 | },
196 | {
197 | url: '/images/exchange-alt-solid.svg',
198 | revision: '8ad4b6e8a9389d116f7d2dbf01be9982',
199 | },
200 | {
201 | url: '/images/tools-solid.svg',
202 | revision: '5eddf805e914ed84f268cc2036844cd4',
203 | },
204 | {
205 | url: '/images/undraw_Live_collaboration_re_60ha.svg',
206 | revision: '23e0edcc8ab1f3c94ab6ab3e1712b5be',
207 | },
208 | {
209 | url: '/images/undraw_at_home_octe.svg',
210 | revision: 'fa3d3993985fc8d3fa854bd67de21aa0',
211 | },
212 | {
213 | url: '/images/undraw_authentication_fsn5.svg',
214 | revision: '06483b459d565893339a7a5efcc09a76',
215 | },
216 | {
217 | url: '/images/undraw_convert_2gjv.svg',
218 | revision: '0bd59cd679d479ff8f8bb09988aa5361',
219 | },
220 | { url: '/manifest.json', revision: '9bfbc8717c285e2e205ba2f808cc4529' },
221 | { url: '/robots.txt', revision: 'db76fe8681f6c19c4a0d3d8ed3bf0227' },
222 | {
223 | url: '/screenshots/ipad-pro.png',
224 | revision: '7d944307fd167215764913dbbec9fc71',
225 | },
226 | {
227 | url: '/screenshots/iphone.png',
228 | revision: 'e72c1f9800bb5c4c5d863833e31f89fe',
229 | },
230 | { url: '/sitemap.xml', revision: 'fc0c32cd7ba520898a8cd9d7a8ee7c95' },
231 | ],
232 | { ignoreURLParametersMatching: [] }
233 | ),
234 | e.cleanupOutdatedCaches(),
235 | e.registerRoute(
236 | '/',
237 | new e.NetworkFirst({
238 | cacheName: 'start-url',
239 | plugins: [
240 | {
241 | cacheWillUpdate: async ({
242 | request: e,
243 | response: s,
244 | event: a,
245 | state: i,
246 | }) =>
247 | s && 'opaqueredirect' === s.type
248 | ? new Response(s.body, {
249 | status: 200,
250 | statusText: 'OK',
251 | headers: s.headers,
252 | })
253 | : s,
254 | },
255 | ],
256 | }),
257 | 'GET'
258 | ),
259 | e.registerRoute(
260 | /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
261 | new e.CacheFirst({
262 | cacheName: 'google-fonts',
263 | plugins: [
264 | new e.ExpirationPlugin({
265 | maxEntries: 4,
266 | maxAgeSeconds: 31536e3,
267 | purgeOnQuotaError: !0,
268 | }),
269 | ],
270 | }),
271 | 'GET'
272 | ),
273 | e.registerRoute(
274 | /\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,
275 | new e.StaleWhileRevalidate({
276 | cacheName: 'static-font-assets',
277 | plugins: [
278 | new e.ExpirationPlugin({
279 | maxEntries: 4,
280 | maxAgeSeconds: 604800,
281 | purgeOnQuotaError: !0,
282 | }),
283 | ],
284 | }),
285 | 'GET'
286 | ),
287 | e.registerRoute(
288 | /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
289 | new e.StaleWhileRevalidate({
290 | cacheName: 'static-image-assets',
291 | plugins: [
292 | new e.ExpirationPlugin({
293 | maxEntries: 64,
294 | maxAgeSeconds: 86400,
295 | purgeOnQuotaError: !0,
296 | }),
297 | ],
298 | }),
299 | 'GET'
300 | ),
301 | e.registerRoute(
302 | /^\/_next\/image\?url=.+$/i,
303 | new e.StaleWhileRevalidate({
304 | cacheName: 'next-image',
305 | plugins: [
306 | new e.ExpirationPlugin({
307 | maxEntries: 64,
308 | maxAgeSeconds: 86400,
309 | purgeOnQuotaError: !0,
310 | }),
311 | ],
312 | }),
313 | 'GET'
314 | ),
315 | e.registerRoute(
316 | /\.(?:mp3|mp4)$/i,
317 | new e.StaleWhileRevalidate({
318 | cacheName: 'static-media-assets',
319 | plugins: [
320 | new e.ExpirationPlugin({
321 | maxEntries: 32,
322 | maxAgeSeconds: 86400,
323 | purgeOnQuotaError: !0,
324 | }),
325 | ],
326 | }),
327 | 'GET'
328 | ),
329 | e.registerRoute(
330 | /\.(?:js)$/i,
331 | new e.StaleWhileRevalidate({
332 | cacheName: 'static-js-assets',
333 | plugins: [
334 | new e.ExpirationPlugin({
335 | maxEntries: 32,
336 | maxAgeSeconds: 86400,
337 | purgeOnQuotaError: !0,
338 | }),
339 | ],
340 | }),
341 | 'GET'
342 | ),
343 | e.registerRoute(
344 | /\.(?:css|less)$/i,
345 | new e.StaleWhileRevalidate({
346 | cacheName: 'static-style-assets',
347 | plugins: [
348 | new e.ExpirationPlugin({
349 | maxEntries: 32,
350 | maxAgeSeconds: 86400,
351 | purgeOnQuotaError: !0,
352 | }),
353 | ],
354 | }),
355 | 'GET'
356 | ),
357 | e.registerRoute(
358 | /\.(?:json|xml|csv)$/i,
359 | new e.NetworkFirst({
360 | cacheName: 'static-data-assets',
361 | plugins: [
362 | new e.ExpirationPlugin({
363 | maxEntries: 32,
364 | maxAgeSeconds: 86400,
365 | purgeOnQuotaError: !0,
366 | }),
367 | ],
368 | }),
369 | 'GET'
370 | ),
371 | e.registerRoute(
372 | /^\/api\/(?!auth\/callback\/).*$/i,
373 | new e.NetworkFirst({
374 | cacheName: 'apis',
375 | networkTimeoutSeconds: 10,
376 | plugins: [
377 | new e.ExpirationPlugin({
378 | maxEntries: 16,
379 | maxAgeSeconds: 86400,
380 | purgeOnQuotaError: !0,
381 | }),
382 | ],
383 | }),
384 | 'GET'
385 | ),
386 | e.registerRoute(
387 | /^\/(?!api\/).*$/i,
388 | new e.NetworkFirst({
389 | cacheName: 'others',
390 | networkTimeoutSeconds: 10,
391 | plugins: [
392 | new e.ExpirationPlugin({
393 | maxEntries: 32,
394 | maxAgeSeconds: 86400,
395 | purgeOnQuotaError: !0,
396 | }),
397 | ],
398 | }),
399 | 'GET'
400 | )
401 | })
402 |
--------------------------------------------------------------------------------
/public/images/undraw_authentication_fsn5.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/workbox-ea903bce.js:
--------------------------------------------------------------------------------
1 | define('./workbox-ea903bce.js', ['exports'], function (t) {
2 | 'use strict'
3 | try {
4 | self['workbox:core:6.1.5'] && _()
5 | } catch (t) {}
6 | const e = (t, ...e) => {
7 | let s = t
8 | return e.length > 0 && (s += ` :: ${JSON.stringify(e)}`), s
9 | }
10 | class s extends Error {
11 | constructor(t, s) {
12 | super(e(t, s)), (this.name = t), (this.details = s)
13 | }
14 | }
15 | try {
16 | self['workbox:routing:6.1.5'] && _()
17 | } catch (t) {}
18 | const n = t => (t && 'object' == typeof t ? t : { handle: t })
19 | class i {
20 | constructor(t, e, s = 'GET') {
21 | ;(this.handler = n(e)), (this.match = t), (this.method = s)
22 | }
23 | setCatchHandler(t) {
24 | this.catchHandler = n(t)
25 | }
26 | }
27 | class r extends i {
28 | constructor(t, e, s) {
29 | super(
30 | ({ url: e }) => {
31 | const s = t.exec(e.href)
32 | if (s && (e.origin === location.origin || 0 === s.index))
33 | return s.slice(1)
34 | },
35 | e,
36 | s
37 | )
38 | }
39 | }
40 | class a {
41 | constructor() {
42 | ;(this.t = new Map()), (this.i = new Map())
43 | }
44 | get routes() {
45 | return this.t
46 | }
47 | addFetchListener() {
48 | self.addEventListener('fetch', t => {
49 | const { request: e } = t,
50 | s = this.handleRequest({ request: e, event: t })
51 | s && t.respondWith(s)
52 | })
53 | }
54 | addCacheListener() {
55 | self.addEventListener('message', t => {
56 | if (t.data && 'CACHE_URLS' === t.data.type) {
57 | const { payload: e } = t.data,
58 | s = Promise.all(
59 | e.urlsToCache.map(e => {
60 | 'string' == typeof e && (e = [e])
61 | const s = new Request(...e)
62 | return this.handleRequest({ request: s, event: t })
63 | })
64 | )
65 | t.waitUntil(s),
66 | t.ports && t.ports[0] && s.then(() => t.ports[0].postMessage(!0))
67 | }
68 | })
69 | }
70 | handleRequest({ request: t, event: e }) {
71 | const s = new URL(t.url, location.href)
72 | if (!s.protocol.startsWith('http')) return
73 | const n = s.origin === location.origin,
74 | { params: i, route: r } = this.findMatchingRoute({
75 | event: e,
76 | request: t,
77 | sameOrigin: n,
78 | url: s,
79 | })
80 | let a = r && r.handler
81 | const c = t.method
82 | if ((!a && this.i.has(c) && (a = this.i.get(c)), !a)) return
83 | let o
84 | try {
85 | o = a.handle({ url: s, request: t, event: e, params: i })
86 | } catch (t) {
87 | o = Promise.reject(t)
88 | }
89 | const h = r && r.catchHandler
90 | return (
91 | o instanceof Promise &&
92 | (this.o || h) &&
93 | (o = o.catch(async n => {
94 | if (h)
95 | try {
96 | return await h.handle({
97 | url: s,
98 | request: t,
99 | event: e,
100 | params: i,
101 | })
102 | } catch (t) {
103 | n = t
104 | }
105 | if (this.o) return this.o.handle({ url: s, request: t, event: e })
106 | throw n
107 | })),
108 | o
109 | )
110 | }
111 | findMatchingRoute({ url: t, sameOrigin: e, request: s, event: n }) {
112 | const i = this.t.get(s.method) || []
113 | for (const r of i) {
114 | let i
115 | const a = r.match({ url: t, sameOrigin: e, request: s, event: n })
116 | if (a)
117 | return (
118 | (i = a),
119 | ((Array.isArray(a) && 0 === a.length) ||
120 | (a.constructor === Object && 0 === Object.keys(a).length) ||
121 | 'boolean' == typeof a) &&
122 | (i = void 0),
123 | { route: r, params: i }
124 | )
125 | }
126 | return {}
127 | }
128 | setDefaultHandler(t, e = 'GET') {
129 | this.i.set(e, n(t))
130 | }
131 | setCatchHandler(t) {
132 | this.o = n(t)
133 | }
134 | registerRoute(t) {
135 | this.t.has(t.method) || this.t.set(t.method, []),
136 | this.t.get(t.method).push(t)
137 | }
138 | unregisterRoute(t) {
139 | if (!this.t.has(t.method))
140 | throw new s('unregister-route-but-not-found-with-method', {
141 | method: t.method,
142 | })
143 | const e = this.t.get(t.method).indexOf(t)
144 | if (!(e > -1)) throw new s('unregister-route-route-not-registered')
145 | this.t.get(t.method).splice(e, 1)
146 | }
147 | }
148 | let c
149 | const o = () => (
150 | c || ((c = new a()), c.addFetchListener(), c.addCacheListener()), c
151 | )
152 | function h(t, e, n) {
153 | let a
154 | if ('string' == typeof t) {
155 | const s = new URL(t, location.href)
156 | a = new i(({ url: t }) => t.href === s.href, e, n)
157 | } else if (t instanceof RegExp) a = new r(t, e, n)
158 | else if ('function' == typeof t) a = new i(t, e, n)
159 | else {
160 | if (!(t instanceof i))
161 | throw new s('unsupported-route-type', {
162 | moduleName: 'workbox-routing',
163 | funcName: 'registerRoute',
164 | paramName: 'capture',
165 | })
166 | a = t
167 | }
168 | return o().registerRoute(a), a
169 | }
170 | try {
171 | self['workbox:strategies:6.1.5'] && _()
172 | } catch (t) {}
173 | const u = {
174 | cacheWillUpdate: async ({ response: t }) =>
175 | 200 === t.status || 0 === t.status ? t : null,
176 | },
177 | l = {
178 | googleAnalytics: 'googleAnalytics',
179 | precache: 'precache-v2',
180 | prefix: 'workbox',
181 | runtime: 'runtime',
182 | suffix: 'undefined' != typeof registration ? registration.scope : '',
183 | },
184 | f = t => [l.prefix, t, l.suffix].filter(t => t && t.length > 0).join('-'),
185 | w = t => t || f(l.precache),
186 | d = t => t || f(l.runtime)
187 | function p() {
188 | return (p =
189 | Object.assign ||
190 | function (t) {
191 | for (var e = 1; e < arguments.length; e++) {
192 | var s = arguments[e]
193 | for (var n in s)
194 | Object.prototype.hasOwnProperty.call(s, n) && (t[n] = s[n])
195 | }
196 | return t
197 | }).apply(this, arguments)
198 | }
199 | function y(t, e) {
200 | const s = new URL(t)
201 | for (const t of e) s.searchParams.delete(t)
202 | return s.href
203 | }
204 | class m {
205 | constructor() {
206 | this.promise = new Promise((t, e) => {
207 | ;(this.resolve = t), (this.reject = e)
208 | })
209 | }
210 | }
211 | const g = new Set()
212 | function R(t) {
213 | return 'string' == typeof t ? new Request(t) : t
214 | }
215 | class v {
216 | constructor(t, e) {
217 | ;(this.h = {}),
218 | Object.assign(this, e),
219 | (this.event = e.event),
220 | (this.u = t),
221 | (this.l = new m()),
222 | (this.p = []),
223 | (this.m = [...t.plugins]),
224 | (this.g = new Map())
225 | for (const t of this.m) this.g.set(t, {})
226 | this.event.waitUntil(this.l.promise)
227 | }
228 | async fetch(t) {
229 | const { event: e } = this
230 | let n = R(t)
231 | if (
232 | 'navigate' === n.mode &&
233 | e instanceof FetchEvent &&
234 | e.preloadResponse
235 | ) {
236 | const t = await e.preloadResponse
237 | if (t) return t
238 | }
239 | const i = this.hasCallback('fetchDidFail') ? n.clone() : null
240 | try {
241 | for (const t of this.iterateCallbacks('requestWillFetch'))
242 | n = await t({ request: n.clone(), event: e })
243 | } catch (t) {
244 | throw new s('plugin-error-request-will-fetch', { thrownError: t })
245 | }
246 | const r = n.clone()
247 | try {
248 | let t
249 | t = await fetch(n, 'navigate' === n.mode ? void 0 : this.u.fetchOptions)
250 | for (const s of this.iterateCallbacks('fetchDidSucceed'))
251 | t = await s({ event: e, request: r, response: t })
252 | return t
253 | } catch (t) {
254 | throw (
255 | (i &&
256 | (await this.runCallbacks('fetchDidFail', {
257 | error: t,
258 | event: e,
259 | originalRequest: i.clone(),
260 | request: r.clone(),
261 | })),
262 | t)
263 | )
264 | }
265 | }
266 | async fetchAndCachePut(t) {
267 | const e = await this.fetch(t),
268 | s = e.clone()
269 | return this.waitUntil(this.cachePut(t, s)), e
270 | }
271 | async cacheMatch(t) {
272 | const e = R(t)
273 | let s
274 | const { cacheName: n, matchOptions: i } = this.u,
275 | r = await this.getCacheKey(e, 'read'),
276 | a = p({}, i, { cacheName: n })
277 | s = await caches.match(r, a)
278 | for (const t of this.iterateCallbacks('cachedResponseWillBeUsed'))
279 | s =
280 | (await t({
281 | cacheName: n,
282 | matchOptions: i,
283 | cachedResponse: s,
284 | request: r,
285 | event: this.event,
286 | })) || void 0
287 | return s
288 | }
289 | async cachePut(t, e) {
290 | const n = R(t)
291 | var i
292 | await ((i = 0), new Promise(t => setTimeout(t, i)))
293 | const r = await this.getCacheKey(n, 'write')
294 | if (!e)
295 | throw new s('cache-put-with-no-response', {
296 | url:
297 | ((a = r.url),
298 | new URL(String(a), location.href).href.replace(
299 | new RegExp(`^${location.origin}`),
300 | ''
301 | )),
302 | })
303 | var a
304 | const c = await this.R(e)
305 | if (!c) return !1
306 | const { cacheName: o, matchOptions: h } = this.u,
307 | u = await self.caches.open(o),
308 | l = this.hasCallback('cacheDidUpdate'),
309 | f = l
310 | ? await (async function (t, e, s, n) {
311 | const i = y(e.url, s)
312 | if (e.url === i) return t.match(e, n)
313 | const r = p({}, n, { ignoreSearch: !0 }),
314 | a = await t.keys(e, r)
315 | for (const e of a) if (i === y(e.url, s)) return t.match(e, n)
316 | })(u, r.clone(), ['__WB_REVISION__'], h)
317 | : null
318 | try {
319 | await u.put(r, l ? c.clone() : c)
320 | } catch (t) {
321 | throw (
322 | ('QuotaExceededError' === t.name &&
323 | (await (async function () {
324 | for (const t of g) await t()
325 | })()),
326 | t)
327 | )
328 | }
329 | for (const t of this.iterateCallbacks('cacheDidUpdate'))
330 | await t({
331 | cacheName: o,
332 | oldResponse: f,
333 | newResponse: c.clone(),
334 | request: r,
335 | event: this.event,
336 | })
337 | return !0
338 | }
339 | async getCacheKey(t, e) {
340 | if (!this.h[e]) {
341 | let s = t
342 | for (const t of this.iterateCallbacks('cacheKeyWillBeUsed'))
343 | s = R(
344 | await t({
345 | mode: e,
346 | request: s,
347 | event: this.event,
348 | params: this.params,
349 | })
350 | )
351 | this.h[e] = s
352 | }
353 | return this.h[e]
354 | }
355 | hasCallback(t) {
356 | for (const e of this.u.plugins) if (t in e) return !0
357 | return !1
358 | }
359 | async runCallbacks(t, e) {
360 | for (const s of this.iterateCallbacks(t)) await s(e)
361 | }
362 | *iterateCallbacks(t) {
363 | for (const e of this.u.plugins)
364 | if ('function' == typeof e[t]) {
365 | const s = this.g.get(e),
366 | n = n => {
367 | const i = p({}, n, { state: s })
368 | return e[t](i)
369 | }
370 | yield n
371 | }
372 | }
373 | waitUntil(t) {
374 | return this.p.push(t), t
375 | }
376 | async doneWaiting() {
377 | let t
378 | for (; (t = this.p.shift()); ) await t
379 | }
380 | destroy() {
381 | this.l.resolve()
382 | }
383 | async R(t) {
384 | let e = t,
385 | s = !1
386 | for (const t of this.iterateCallbacks('cacheWillUpdate'))
387 | if (
388 | ((e =
389 | (await t({
390 | request: this.request,
391 | response: e,
392 | event: this.event,
393 | })) || void 0),
394 | (s = !0),
395 | !e)
396 | )
397 | break
398 | return s || (e && 200 !== e.status && (e = void 0)), e
399 | }
400 | }
401 | class q {
402 | constructor(t = {}) {
403 | ;(this.cacheName = d(t.cacheName)),
404 | (this.plugins = t.plugins || []),
405 | (this.fetchOptions = t.fetchOptions),
406 | (this.matchOptions = t.matchOptions)
407 | }
408 | handle(t) {
409 | const [e] = this.handleAll(t)
410 | return e
411 | }
412 | handleAll(t) {
413 | t instanceof FetchEvent && (t = { event: t, request: t.request })
414 | const e = t.event,
415 | s = 'string' == typeof t.request ? new Request(t.request) : t.request,
416 | n = 'params' in t ? t.params : void 0,
417 | i = new v(this, { event: e, request: s, params: n }),
418 | r = this.v(i, s, e)
419 | return [r, this.q(r, i, s, e)]
420 | }
421 | async v(t, e, n) {
422 | let i
423 | await t.runCallbacks('handlerWillStart', { event: n, request: e })
424 | try {
425 | if (((i = await this.U(e, t)), !i || 'error' === i.type))
426 | throw new s('no-response', { url: e.url })
427 | } catch (s) {
428 | for (const r of t.iterateCallbacks('handlerDidError'))
429 | if (((i = await r({ error: s, event: n, request: e })), i)) break
430 | if (!i) throw s
431 | }
432 | for (const s of t.iterateCallbacks('handlerWillRespond'))
433 | i = await s({ event: n, request: e, response: i })
434 | return i
435 | }
436 | async q(t, e, s, n) {
437 | let i, r
438 | try {
439 | i = await t
440 | } catch (r) {}
441 | try {
442 | await e.runCallbacks('handlerDidRespond', {
443 | event: n,
444 | request: s,
445 | response: i,
446 | }),
447 | await e.doneWaiting()
448 | } catch (t) {
449 | r = t
450 | }
451 | if (
452 | (await e.runCallbacks('handlerDidComplete', {
453 | event: n,
454 | request: s,
455 | response: i,
456 | error: r,
457 | }),
458 | e.destroy(),
459 | r)
460 | )
461 | throw r
462 | }
463 | }
464 | function U(t) {
465 | t.then(() => {})
466 | }
467 | class x {
468 | constructor(t, e, { onupgradeneeded: s, onversionchange: n } = {}) {
469 | ;(this._ = null),
470 | (this.L = t),
471 | (this.N = e),
472 | (this.C = s),
473 | (this.D = n || (() => this.close()))
474 | }
475 | get db() {
476 | return this._
477 | }
478 | async open() {
479 | if (!this._)
480 | return (
481 | (this._ = await new Promise((t, e) => {
482 | let s = !1
483 | setTimeout(() => {
484 | ;(s = !0),
485 | e(new Error('The open request was blocked and timed out'))
486 | }, this.OPEN_TIMEOUT)
487 | const n = indexedDB.open(this.L, this.N)
488 | ;(n.onerror = () => e(n.error)),
489 | (n.onupgradeneeded = t => {
490 | s
491 | ? (n.transaction.abort(), n.result.close())
492 | : 'function' == typeof this.C && this.C(t)
493 | }),
494 | (n.onsuccess = () => {
495 | const e = n.result
496 | s ? e.close() : ((e.onversionchange = this.D.bind(this)), t(e))
497 | })
498 | })),
499 | this
500 | )
501 | }
502 | async getKey(t, e) {
503 | return (await this.getAllKeys(t, e, 1))[0]
504 | }
505 | async getAll(t, e, s) {
506 | return await this.getAllMatching(t, { query: e, count: s })
507 | }
508 | async getAllKeys(t, e, s) {
509 | return (
510 | await this.getAllMatching(t, { query: e, count: s, includeKeys: !0 })
511 | ).map(t => t.key)
512 | }
513 | async getAllMatching(
514 | t,
515 | {
516 | index: e,
517 | query: s = null,
518 | direction: n = 'next',
519 | count: i,
520 | includeKeys: r = !1,
521 | } = {}
522 | ) {
523 | return await this.transaction([t], 'readonly', (a, c) => {
524 | const o = a.objectStore(t),
525 | h = e ? o.index(e) : o,
526 | u = [],
527 | l = h.openCursor(s, n)
528 | l.onsuccess = () => {
529 | const t = l.result
530 | t
531 | ? (u.push(r ? t : t.value),
532 | i && u.length >= i ? c(u) : t.continue())
533 | : c(u)
534 | }
535 | })
536 | }
537 | async transaction(t, e, s) {
538 | return (
539 | await this.open(),
540 | await new Promise((n, i) => {
541 | const r = this._.transaction(t, e)
542 | ;(r.onabort = () => i(r.error)),
543 | (r.oncomplete = () => n()),
544 | s(r, t => n(t))
545 | })
546 | )
547 | }
548 | async T(t, e, s, ...n) {
549 | return await this.transaction([e], s, (s, i) => {
550 | const r = s.objectStore(e),
551 | a = r[t].apply(r, n)
552 | a.onsuccess = () => i(a.result)
553 | })
554 | }
555 | close() {
556 | this._ && (this._.close(), (this._ = null))
557 | }
558 | }
559 | x.prototype.OPEN_TIMEOUT = 2e3
560 | const b = {
561 | readonly: ['get', 'count', 'getKey', 'getAll', 'getAllKeys'],
562 | readwrite: ['add', 'put', 'clear', 'delete'],
563 | }
564 | for (const [t, e] of Object.entries(b))
565 | for (const s of e)
566 | s in IDBObjectStore.prototype &&
567 | (x.prototype[s] = async function (e, ...n) {
568 | return await this.T(s, e, t, ...n)
569 | })
570 | try {
571 | self['workbox:expiration:6.1.5'] && _()
572 | } catch (t) {}
573 | const L = 'cache-entries',
574 | N = t => {
575 | const e = new URL(t, location.href)
576 | return (e.hash = ''), e.href
577 | }
578 | class C {
579 | constructor(t) {
580 | ;(this.P = t),
581 | (this._ = new x('workbox-expiration', 1, {
582 | onupgradeneeded: t => this.k(t),
583 | }))
584 | }
585 | k(t) {
586 | const e = t.target.result.createObjectStore(L, { keyPath: 'id' })
587 | e.createIndex('cacheName', 'cacheName', { unique: !1 }),
588 | e.createIndex('timestamp', 'timestamp', { unique: !1 }),
589 | (async t => {
590 | await new Promise((e, s) => {
591 | const n = indexedDB.deleteDatabase(t)
592 | ;(n.onerror = () => {
593 | s(n.error)
594 | }),
595 | (n.onblocked = () => {
596 | s(new Error('Delete blocked'))
597 | }),
598 | (n.onsuccess = () => {
599 | e()
600 | })
601 | })
602 | })(this.P)
603 | }
604 | async setTimestamp(t, e) {
605 | const s = {
606 | url: (t = N(t)),
607 | timestamp: e,
608 | cacheName: this.P,
609 | id: this.K(t),
610 | }
611 | await this._.put(L, s)
612 | }
613 | async getTimestamp(t) {
614 | return (await this._.get(L, this.K(t))).timestamp
615 | }
616 | async expireEntries(t, e) {
617 | const s = await this._.transaction(L, 'readwrite', (s, n) => {
618 | const i = s
619 | .objectStore(L)
620 | .index('timestamp')
621 | .openCursor(null, 'prev'),
622 | r = []
623 | let a = 0
624 | i.onsuccess = () => {
625 | const s = i.result
626 | if (s) {
627 | const n = s.value
628 | n.cacheName === this.P &&
629 | ((t && n.timestamp < t) || (e && a >= e)
630 | ? r.push(s.value)
631 | : a++),
632 | s.continue()
633 | } else n(r)
634 | }
635 | }),
636 | n = []
637 | for (const t of s) await this._.delete(L, t.id), n.push(t.url)
638 | return n
639 | }
640 | K(t) {
641 | return this.P + '|' + N(t)
642 | }
643 | }
644 | class E {
645 | constructor(t, e = {}) {
646 | ;(this.O = !1),
647 | (this.W = !1),
648 | (this.M = e.maxEntries),
649 | (this.A = e.maxAgeSeconds),
650 | (this.S = e.matchOptions),
651 | (this.P = t),
652 | (this.I = new C(t))
653 | }
654 | async expireEntries() {
655 | if (this.O) return void (this.W = !0)
656 | this.O = !0
657 | const t = this.A ? Date.now() - 1e3 * this.A : 0,
658 | e = await this.I.expireEntries(t, this.M),
659 | s = await self.caches.open(this.P)
660 | for (const t of e) await s.delete(t, this.S)
661 | ;(this.O = !1), this.W && ((this.W = !1), U(this.expireEntries()))
662 | }
663 | async updateTimestamp(t) {
664 | await this.I.setTimestamp(t, Date.now())
665 | }
666 | async isURLExpired(t) {
667 | if (this.A) {
668 | return (await this.I.getTimestamp(t)) < Date.now() - 1e3 * this.A
669 | }
670 | return !1
671 | }
672 | async delete() {
673 | ;(this.W = !1), await this.I.expireEntries(1 / 0)
674 | }
675 | }
676 | function D(t, e) {
677 | const s = e()
678 | return t.waitUntil(s), s
679 | }
680 | try {
681 | self['workbox:precaching:6.1.5'] && _()
682 | } catch (t) {}
683 | function T(t) {
684 | if (!t) throw new s('add-to-cache-list-unexpected-type', { entry: t })
685 | if ('string' == typeof t) {
686 | const e = new URL(t, location.href)
687 | return { cacheKey: e.href, url: e.href }
688 | }
689 | const { revision: e, url: n } = t
690 | if (!n) throw new s('add-to-cache-list-unexpected-type', { entry: t })
691 | if (!e) {
692 | const t = new URL(n, location.href)
693 | return { cacheKey: t.href, url: t.href }
694 | }
695 | const i = new URL(n, location.href),
696 | r = new URL(n, location.href)
697 | return (
698 | i.searchParams.set('__WB_REVISION__', e),
699 | { cacheKey: i.href, url: r.href }
700 | )
701 | }
702 | class P {
703 | constructor() {
704 | ;(this.updatedURLs = []),
705 | (this.notUpdatedURLs = []),
706 | (this.handlerWillStart = async ({ request: t, state: e }) => {
707 | e && (e.originalRequest = t)
708 | }),
709 | (this.cachedResponseWillBeUsed = async ({
710 | event: t,
711 | state: e,
712 | cachedResponse: s,
713 | }) => {
714 | if ('install' === t.type) {
715 | const t = e.originalRequest.url
716 | s ? this.notUpdatedURLs.push(t) : this.updatedURLs.push(t)
717 | }
718 | return s
719 | })
720 | }
721 | }
722 | class k {
723 | constructor({ precacheController: t }) {
724 | ;(this.cacheKeyWillBeUsed = async ({ request: t, params: e }) => {
725 | const s = (e && e.cacheKey) || this.j.getCacheKeyForURL(t.url)
726 | return s ? new Request(s) : t
727 | }),
728 | (this.j = t)
729 | }
730 | }
731 | let K, O
732 | async function W(t, e) {
733 | let n = null
734 | if (t.url) {
735 | n = new URL(t.url).origin
736 | }
737 | if (n !== self.location.origin)
738 | throw new s('cross-origin-copy-response', { origin: n })
739 | const i = t.clone(),
740 | r = {
741 | headers: new Headers(i.headers),
742 | status: i.status,
743 | statusText: i.statusText,
744 | },
745 | a = e ? e(r) : r,
746 | c = (function () {
747 | if (void 0 === K) {
748 | const t = new Response('')
749 | if ('body' in t)
750 | try {
751 | new Response(t.body), (K = !0)
752 | } catch (t) {
753 | K = !1
754 | }
755 | K = !1
756 | }
757 | return K
758 | })()
759 | ? i.body
760 | : await i.blob()
761 | return new Response(c, a)
762 | }
763 | class M extends q {
764 | constructor(t = {}) {
765 | ;(t.cacheName = w(t.cacheName)),
766 | super(t),
767 | (this.F = !1 !== t.fallbackToNetwork),
768 | this.plugins.push(M.copyRedirectedCacheableResponsesPlugin)
769 | }
770 | async U(t, e) {
771 | const s = await e.cacheMatch(t)
772 | return (
773 | s ||
774 | (e.event && 'install' === e.event.type
775 | ? await this.B(t, e)
776 | : await this.H(t, e))
777 | )
778 | }
779 | async H(t, e) {
780 | let n
781 | if (!this.F)
782 | throw new s('missing-precache-entry', {
783 | cacheName: this.cacheName,
784 | url: t.url,
785 | })
786 | return (n = await e.fetch(t)), n
787 | }
788 | async B(t, e) {
789 | this.$()
790 | const n = await e.fetch(t)
791 | if (!(await e.cachePut(t, n.clone())))
792 | throw new s('bad-precaching-response', { url: t.url, status: n.status })
793 | return n
794 | }
795 | $() {
796 | let t = null,
797 | e = 0
798 | for (const [s, n] of this.plugins.entries())
799 | n !== M.copyRedirectedCacheableResponsesPlugin &&
800 | (n === M.defaultPrecacheCacheabilityPlugin && (t = s),
801 | n.cacheWillUpdate && e++)
802 | 0 === e
803 | ? this.plugins.push(M.defaultPrecacheCacheabilityPlugin)
804 | : e > 1 && null !== t && this.plugins.splice(t, 1)
805 | }
806 | }
807 | ;(M.defaultPrecacheCacheabilityPlugin = {
808 | cacheWillUpdate: async ({ response: t }) =>
809 | !t || t.status >= 400 ? null : t,
810 | }),
811 | (M.copyRedirectedCacheableResponsesPlugin = {
812 | cacheWillUpdate: async ({ response: t }) =>
813 | t.redirected ? await W(t) : t,
814 | })
815 | class A {
816 | constructor({
817 | cacheName: t,
818 | plugins: e = [],
819 | fallbackToNetwork: s = !0,
820 | } = {}) {
821 | ;(this.G = new Map()),
822 | (this.V = new Map()),
823 | (this.J = new Map()),
824 | (this.u = new M({
825 | cacheName: w(t),
826 | plugins: [...e, new k({ precacheController: this })],
827 | fallbackToNetwork: s,
828 | })),
829 | (this.install = this.install.bind(this)),
830 | (this.activate = this.activate.bind(this))
831 | }
832 | get strategy() {
833 | return this.u
834 | }
835 | precache(t) {
836 | this.addToCacheList(t),
837 | this.X ||
838 | (self.addEventListener('install', this.install),
839 | self.addEventListener('activate', this.activate),
840 | (this.X = !0))
841 | }
842 | addToCacheList(t) {
843 | const e = []
844 | for (const n of t) {
845 | 'string' == typeof n
846 | ? e.push(n)
847 | : n && void 0 === n.revision && e.push(n.url)
848 | const { cacheKey: t, url: i } = T(n),
849 | r = 'string' != typeof n && n.revision ? 'reload' : 'default'
850 | if (this.G.has(i) && this.G.get(i) !== t)
851 | throw new s('add-to-cache-list-conflicting-entries', {
852 | firstEntry: this.G.get(i),
853 | secondEntry: t,
854 | })
855 | if ('string' != typeof n && n.integrity) {
856 | if (this.J.has(t) && this.J.get(t) !== n.integrity)
857 | throw new s('add-to-cache-list-conflicting-integrities', { url: i })
858 | this.J.set(t, n.integrity)
859 | }
860 | if ((this.G.set(i, t), this.V.set(i, r), e.length > 0)) {
861 | const t = `Workbox is precaching URLs without revision info: ${e.join(
862 | ', '
863 | )}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`
864 | console.warn(t)
865 | }
866 | }
867 | }
868 | install(t) {
869 | return D(t, async () => {
870 | const e = new P()
871 | this.strategy.plugins.push(e)
872 | for (const [e, s] of this.G) {
873 | const n = this.J.get(s),
874 | i = this.V.get(e),
875 | r = new Request(e, {
876 | integrity: n,
877 | cache: i,
878 | credentials: 'same-origin',
879 | })
880 | await Promise.all(
881 | this.strategy.handleAll({
882 | params: { cacheKey: s },
883 | request: r,
884 | event: t,
885 | })
886 | )
887 | }
888 | const { updatedURLs: s, notUpdatedURLs: n } = e
889 | return { updatedURLs: s, notUpdatedURLs: n }
890 | })
891 | }
892 | activate(t) {
893 | return D(t, async () => {
894 | const t = await self.caches.open(this.strategy.cacheName),
895 | e = await t.keys(),
896 | s = new Set(this.G.values()),
897 | n = []
898 | for (const i of e) s.has(i.url) || (await t.delete(i), n.push(i.url))
899 | return { deletedURLs: n }
900 | })
901 | }
902 | getURLsToCacheKeys() {
903 | return this.G
904 | }
905 | getCachedURLs() {
906 | return [...this.G.keys()]
907 | }
908 | getCacheKeyForURL(t) {
909 | const e = new URL(t, location.href)
910 | return this.G.get(e.href)
911 | }
912 | async matchPrecache(t) {
913 | const e = t instanceof Request ? t.url : t,
914 | s = this.getCacheKeyForURL(e)
915 | if (s) {
916 | return (await self.caches.open(this.strategy.cacheName)).match(s)
917 | }
918 | }
919 | createHandlerBoundToURL(t) {
920 | const e = this.getCacheKeyForURL(t)
921 | if (!e) throw new s('non-precached-url', { url: t })
922 | return s => (
923 | (s.request = new Request(t)),
924 | (s.params = p({ cacheKey: e }, s.params)),
925 | this.strategy.handle(s)
926 | )
927 | }
928 | }
929 | const S = () => (O || (O = new A()), O)
930 | class I extends i {
931 | constructor(t, e) {
932 | super(({ request: s }) => {
933 | const n = t.getURLsToCacheKeys()
934 | for (const t of (function* (
935 | t,
936 | {
937 | ignoreURLParametersMatching: e = [/^utm_/, /^fbclid$/],
938 | directoryIndex: s = 'index.html',
939 | cleanURLs: n = !0,
940 | urlManipulation: i,
941 | } = {}
942 | ) {
943 | const r = new URL(t, location.href)
944 | ;(r.hash = ''), yield r.href
945 | const a = (function (t, e = []) {
946 | for (const s of [...t.searchParams.keys()])
947 | e.some(t => t.test(s)) && t.searchParams.delete(s)
948 | return t
949 | })(r, e)
950 | if ((yield a.href, s && a.pathname.endsWith('/'))) {
951 | const t = new URL(a.href)
952 | ;(t.pathname += s), yield t.href
953 | }
954 | if (n) {
955 | const t = new URL(a.href)
956 | ;(t.pathname += '.html'), yield t.href
957 | }
958 | if (i) {
959 | const t = i({ url: r })
960 | for (const e of t) yield e.href
961 | }
962 | })(s.url, e)) {
963 | const e = n.get(t)
964 | if (e) return { cacheKey: e }
965 | }
966 | }, t.strategy)
967 | }
968 | }
969 | ;(t.CacheFirst = class extends q {
970 | async U(t, e) {
971 | let n,
972 | i = await e.cacheMatch(t)
973 | if (!i)
974 | try {
975 | i = await e.fetchAndCachePut(t)
976 | } catch (t) {
977 | n = t
978 | }
979 | if (!i) throw new s('no-response', { url: t.url, error: n })
980 | return i
981 | }
982 | }),
983 | (t.ExpirationPlugin = class {
984 | constructor(t = {}) {
985 | var e
986 | ;(this.cachedResponseWillBeUsed = async ({
987 | event: t,
988 | request: e,
989 | cacheName: s,
990 | cachedResponse: n,
991 | }) => {
992 | if (!n) return null
993 | const i = this.Y(n),
994 | r = this.Z(s)
995 | U(r.expireEntries())
996 | const a = r.updateTimestamp(e.url)
997 | if (t)
998 | try {
999 | t.waitUntil(a)
1000 | } catch (t) {}
1001 | return i ? n : null
1002 | }),
1003 | (this.cacheDidUpdate = async ({ cacheName: t, request: e }) => {
1004 | const s = this.Z(t)
1005 | await s.updateTimestamp(e.url), await s.expireEntries()
1006 | }),
1007 | (this.tt = t),
1008 | (this.A = t.maxAgeSeconds),
1009 | (this.et = new Map()),
1010 | t.purgeOnQuotaError &&
1011 | ((e = () => this.deleteCacheAndMetadata()), g.add(e))
1012 | }
1013 | Z(t) {
1014 | if (t === d()) throw new s('expire-custom-caches-only')
1015 | let e = this.et.get(t)
1016 | return e || ((e = new E(t, this.tt)), this.et.set(t, e)), e
1017 | }
1018 | Y(t) {
1019 | if (!this.A) return !0
1020 | const e = this.st(t)
1021 | if (null === e) return !0
1022 | return e >= Date.now() - 1e3 * this.A
1023 | }
1024 | st(t) {
1025 | if (!t.headers.has('date')) return null
1026 | const e = t.headers.get('date'),
1027 | s = new Date(e).getTime()
1028 | return isNaN(s) ? null : s
1029 | }
1030 | async deleteCacheAndMetadata() {
1031 | for (const [t, e] of this.et)
1032 | await self.caches.delete(t), await e.delete()
1033 | this.et = new Map()
1034 | }
1035 | }),
1036 | (t.NetworkFirst = class extends q {
1037 | constructor(t = {}) {
1038 | super(t),
1039 | this.plugins.some(t => 'cacheWillUpdate' in t) ||
1040 | this.plugins.unshift(u),
1041 | (this.nt = t.networkTimeoutSeconds || 0)
1042 | }
1043 | async U(t, e) {
1044 | const n = [],
1045 | i = []
1046 | let r
1047 | if (this.nt) {
1048 | const { id: s, promise: a } = this.it({
1049 | request: t,
1050 | logs: n,
1051 | handler: e,
1052 | })
1053 | ;(r = s), i.push(a)
1054 | }
1055 | const a = this.rt({ timeoutId: r, request: t, logs: n, handler: e })
1056 | i.push(a)
1057 | const c = await e.waitUntil(
1058 | (async () => (await e.waitUntil(Promise.race(i))) || (await a))()
1059 | )
1060 | if (!c) throw new s('no-response', { url: t.url })
1061 | return c
1062 | }
1063 | it({ request: t, logs: e, handler: s }) {
1064 | let n
1065 | return {
1066 | promise: new Promise(e => {
1067 | n = setTimeout(async () => {
1068 | e(await s.cacheMatch(t))
1069 | }, 1e3 * this.nt)
1070 | }),
1071 | id: n,
1072 | }
1073 | }
1074 | async rt({ timeoutId: t, request: e, logs: s, handler: n }) {
1075 | let i, r
1076 | try {
1077 | r = await n.fetchAndCachePut(e)
1078 | } catch (t) {
1079 | i = t
1080 | }
1081 | return t && clearTimeout(t), (!i && r) || (r = await n.cacheMatch(e)), r
1082 | }
1083 | }),
1084 | (t.StaleWhileRevalidate = class extends q {
1085 | constructor(t) {
1086 | super(t),
1087 | this.plugins.some(t => 'cacheWillUpdate' in t) ||
1088 | this.plugins.unshift(u)
1089 | }
1090 | async U(t, e) {
1091 | const n = e.fetchAndCachePut(t).catch(() => {})
1092 | let i,
1093 | r = await e.cacheMatch(t)
1094 | if (r);
1095 | else
1096 | try {
1097 | r = await n
1098 | } catch (t) {
1099 | i = t
1100 | }
1101 | if (!r) throw new s('no-response', { url: t.url, error: i })
1102 | return r
1103 | }
1104 | }),
1105 | (t.cleanupOutdatedCaches = function () {
1106 | self.addEventListener('activate', t => {
1107 | const e = w()
1108 | t.waitUntil(
1109 | (async (t, e = '-precache-') => {
1110 | const s = (await self.caches.keys()).filter(
1111 | s =>
1112 | s.includes(e) && s.includes(self.registration.scope) && s !== t
1113 | )
1114 | return await Promise.all(s.map(t => self.caches.delete(t))), s
1115 | })(e).then(t => {})
1116 | )
1117 | })
1118 | }),
1119 | (t.clientsClaim = function () {
1120 | self.addEventListener('activate', () => self.clients.claim())
1121 | }),
1122 | (t.precacheAndRoute = function (t, e) {
1123 | !(function (t) {
1124 | S().precache(t)
1125 | })(t),
1126 | (function (t) {
1127 | const e = S()
1128 | h(new I(e, t))
1129 | })(e)
1130 | }),
1131 | (t.registerRoute = h)
1132 | })
1133 |
--------------------------------------------------------------------------------