mirror of
				https://github.com/CorentinTh/it-tools.git
				synced 2025-10-31 12:03:48 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			card-hover
			...
			update-dep
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | facc0b5d9e | 
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -31,4 +31,6 @@ jobs: | ||||
|         run: pnpm typecheck | ||||
|  | ||||
|       - name: Build the app | ||||
|         env: | ||||
|           NODE_OPTIONS: --max-old-space-size=4096 | ||||
|         run: pnpm build | ||||
|   | ||||
							
								
								
									
										13
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.github/workflows/e2e-tests.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,13 +1,13 @@ | ||||
| name: E2E tests | ||||
| on: | ||||
|   pull_request: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
| on: [deployment_status] | ||||
|  | ||||
| jobs: | ||||
|   test: | ||||
|     if: github.event.deployment_status.state == 'success' | ||||
|     timeout-minutes: 60 | ||||
|     runs-on: ubuntu-latest | ||||
|     env: | ||||
|       BASE_URL: ${{ github.event.deployment_status.target_url }} | ||||
|     strategy: | ||||
|       matrix: | ||||
|         shard: [1/3, 2/3, 3/3] | ||||
| @@ -28,9 +28,6 @@ jobs: | ||||
|       - name: Install dependencies | ||||
|         run: pnpm install | ||||
|  | ||||
|       - name: Build app | ||||
|         run: pnpm build | ||||
|  | ||||
|       - name: Restore Playwright browsers from cache | ||||
|         uses: actions/cache@v3 | ||||
|         with: | ||||
|   | ||||
							
								
								
									
										8
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -32,8 +32,6 @@ declare module '@vue/runtime-core' { | ||||
|     Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] | ||||
|     CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default'] | ||||
|     'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] | ||||
|     CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default'] | ||||
|     CKeyValueListItem: typeof import('./src/ui/c-key-value-list/c-key-value-list-item.vue')['default'] | ||||
|     CLabel: typeof import('./src/ui/c-label/c-label.vue')['default'] | ||||
|     CLink: typeof import('./src/ui/c-link/c-link.vue')['default'] | ||||
|     'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] | ||||
| @@ -47,10 +45,6 @@ declare module '@vue/runtime-core' { | ||||
|     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] | ||||
|     CSelect: typeof import('./src/ui/c-select/c-select.vue')['default'] | ||||
|     'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default'] | ||||
|     CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default'] | ||||
|     'CTextCopyable.demo': typeof import('./src/ui/c-text-copyable/c-text-copyable.demo.vue')['default'] | ||||
|     CTooltip: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default'] | ||||
|     'CTooltip.demo': typeof import('./src/ui/c-tooltip/c-tooltip.demo.vue')['default'] | ||||
|     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] | ||||
|     'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default'] | ||||
|     DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] | ||||
| @@ -74,7 +68,6 @@ declare module '@vue/runtime-core' { | ||||
|     HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default'] | ||||
|     HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default'] | ||||
|     HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] | ||||
|     IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default'] | ||||
|     'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] | ||||
|     'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default'] | ||||
|     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] | ||||
| @@ -188,7 +181,6 @@ declare module '@vue/runtime-core' { | ||||
|     UserAgentParser: typeof import('./src/tools/user-agent-parser/user-agent-parser.vue')['default'] | ||||
|     UserAgentResultCards: typeof import('./src/tools/user-agent-parser/user-agent-result-cards.vue')['default'] | ||||
|     UuidGenerator: typeof import('./src/tools/uuid-generator/uuid-generator.vue')['default'] | ||||
|     WifiQrCodeGenerator: typeof import('./src/tools/wifi-qr-code-generator/wifi-qr-code-generator.vue')['default'] | ||||
|     XmlFormatter: typeof import('./src/tools/xml-formatter/xml-formatter.vue')['default'] | ||||
|     YamlToJson: typeof import('./src/tools/yaml-to-json-converter/yaml-to-json.vue')['default'] | ||||
|     YamlToToml: typeof import('./src/tools/yaml-to-toml/yaml-to-toml.vue')['default'] | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| home: | ||||
|   categories: | ||||
|     newestTools: Newest tools | ||||
|     allTheTools: All the tools | ||||
|     yourFavoriteTools: Your favorite tools | ||||
|  | ||||
|   | ||||
							
								
								
									
										108
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								package.json
									
									
									
									
									
								
							| @@ -21,12 +21,11 @@ | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "dev": "vite", | ||||
|     "build": "vue-tsc --noEmit && NODE_OPTIONS=--max_old_space_size=4096 vite build", | ||||
|     "build": "vue-tsc --noEmit && vite build", | ||||
|     "preview": "vite preview --port 5050", | ||||
|     "test": "npm run test:unit", | ||||
|     "test:unit": "vitest --environment jsdom", | ||||
|     "test:e2e": "playwright test", | ||||
|     "test:e2e:dev": "BASE_URL=http://localhost:5173 NO_WEB_SERVER=true playwright test", | ||||
|     "coverage": "vitest run --coverage", | ||||
|     "typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", | ||||
|     "lint": "eslint src --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .gitignore", | ||||
| @@ -36,100 +35,99 @@ | ||||
|   "dependencies": { | ||||
|     "@it-tools/bip39": "^0.0.4", | ||||
|     "@it-tools/oggen": "^1.3.0", | ||||
|     "@sindresorhus/slugify": "^2.2.0", | ||||
|     "@tiptap/pm": "^2.1.6", | ||||
|     "@tiptap/starter-kit": "^2.1.6", | ||||
|     "@tiptap/vue-3": "^2.0.3", | ||||
|     "@sindresorhus/slugify": "^2.2.1", | ||||
|     "@tiptap/pm": "^2.1.7", | ||||
|     "@tiptap/starter-kit": "^2.1.7", | ||||
|     "@tiptap/vue-3": "^2.1.7", | ||||
|     "@vicons/material": "^0.12.0", | ||||
|     "@vicons/tabler": "^0.12.0", | ||||
|     "@vueuse/core": "^10.3.0", | ||||
|     "@vueuse/head": "^1.0.0", | ||||
|     "@vueuse/router": "^10.0.0", | ||||
|     "@vueuse/core": "^10.4.0", | ||||
|     "@vueuse/head": "^1.3.1", | ||||
|     "@vueuse/router": "^10.4.0", | ||||
|     "bcryptjs": "^2.4.3", | ||||
|     "change-case": "^4.1.2", | ||||
|     "colord": "^2.9.3", | ||||
|     "composerize-ts": "^0.6.2", | ||||
|     "country-code-lookup": "^0.1.0", | ||||
|     "cron-validator": "^1.3.1", | ||||
|     "cronstrue": "^2.26.0", | ||||
|     "cronstrue": "^2.31.0", | ||||
|     "crypto-js": "^4.1.1", | ||||
|     "date-fns": "^2.29.3", | ||||
|     "date-fns": "^2.30.0", | ||||
|     "emojilib": "^3.0.10", | ||||
|     "figue": "^1.2.0", | ||||
|     "fuse.js": "^6.6.2", | ||||
|     "highlight.js": "^11.7.0", | ||||
|     "highlight.js": "^11.8.0", | ||||
|     "iarna-toml-esm": "^3.0.5", | ||||
|     "ibantools": "^4.3.3", | ||||
|     "json5": "^2.2.3", | ||||
|     "jwt-decode": "^3.1.2", | ||||
|     "libphonenumber-js": "^1.10.28", | ||||
|     "libphonenumber-js": "^1.10.41", | ||||
|     "lodash": "^4.17.21", | ||||
|     "mathjs": "^11.9.1", | ||||
|     "mathjs": "^11.10.0", | ||||
|     "mime-types": "^2.1.35", | ||||
|     "monaco-editor": "^0.41.0", | ||||
|     "naive-ui": "^2.34.3", | ||||
|     "naive-ui": "^2.34.4", | ||||
|     "netmask": "^2.0.2", | ||||
|     "node-forge": "^1.3.1", | ||||
|     "oui": "^12.0.52", | ||||
|     "pinia": "^2.0.34", | ||||
|     "oui": "^12.0.71", | ||||
|     "pinia": "^2.1.6", | ||||
|     "plausible-tracker": "^0.3.8", | ||||
|     "qrcode": "^1.5.1", | ||||
|     "qrcode": "^1.5.3", | ||||
|     "randombytes": "^2.1.0", | ||||
|     "sql-formatter": "^13.0.0", | ||||
|     "sql-formatter": "^12.2.4", | ||||
|     "ua-parser-js": "^1.0.35", | ||||
|     "unicode-emoji-json": "^0.4.0", | ||||
|     "unplugin-auto-import": "^0.16.4", | ||||
|     "unplugin-auto-import": "^0.16.6", | ||||
|     "uuid": "^9.0.0", | ||||
|     "vue": "^3.3.4", | ||||
|     "vue-i18n": "^9.2.2", | ||||
|     "vue-router": "^4.1.6", | ||||
|     "vue-tsc": "^1.8.1", | ||||
|     "xml-formatter": "^3.3.2", | ||||
|     "yaml": "^2.2.1" | ||||
|     "vue-router": "^4.2.4", | ||||
|     "vue-tsc": "^1.8.8", | ||||
|     "xml-formatter": "^3.5.0", | ||||
|     "yaml": "^2.3.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@antfu/eslint-config": "^0.41.0", | ||||
|     "@iconify-json/mdi": "^1.1.50", | ||||
|     "@intlify/unplugin-vue-i18n": "^0.13.0", | ||||
|     "@playwright/test": "^1.32.3", | ||||
|     "@rushstack/eslint-patch": "^1.2.0", | ||||
|     "@tsconfig/node18": "^18.2.0", | ||||
|     "@iconify-json/mdi": "^1.1.54", | ||||
|     "@intlify/unplugin-vue-i18n": "^0.12.3", | ||||
|     "@playwright/test": "^1.37.1", | ||||
|     "@rushstack/eslint-patch": "^1.3.3", | ||||
|     "@tsconfig/node18": "^18.2.1", | ||||
|     "@types/bcryptjs": "^2.4.2", | ||||
|     "@types/crypto-js": "^4.1.1", | ||||
|     "@types/jsdom": "^21.0.0", | ||||
|     "@types/lodash": "^4.14.192", | ||||
|     "@types/jsdom": "^21.1.2", | ||||
|     "@types/lodash": "^4.14.197", | ||||
|     "@types/mime-types": "^2.1.1", | ||||
|     "@types/netmask": "^2.0.0", | ||||
|     "@types/node": "^18.15.11", | ||||
|     "@types/node-forge": "^1.3.2", | ||||
|     "@types/qrcode": "^1.5.0", | ||||
|     "@types/netmask": "^2.0.2", | ||||
|     "@types/node": "^18.17.11", | ||||
|     "@types/node-forge": "^1.3.4", | ||||
|     "@types/qrcode": "^1.5.1", | ||||
|     "@types/randombytes": "^2.0.0", | ||||
|     "@types/ua-parser-js": "^0.7.36", | ||||
|     "@types/uuid": "^9.0.0", | ||||
|     "@unocss/eslint-config": "^0.55.0", | ||||
|     "@vitejs/plugin-vue": "^4.3.2", | ||||
|     "@types/uuid": "^9.0.2", | ||||
|     "@unocss/eslint-config": "^0.55.3", | ||||
|     "@vitejs/plugin-vue": "^4.3.3", | ||||
|     "@vitejs/plugin-vue-jsx": "^3.0.2", | ||||
|     "@vue/compiler-sfc": "^3.2.47", | ||||
|     "@vue/compiler-sfc": "^3.3.4", | ||||
|     "@vue/runtime-dom": "^3.3.4", | ||||
|     "@vue/test-utils": "^2.3.2", | ||||
|     "@vue/test-utils": "^2.4.1", | ||||
|     "@vue/tsconfig": "^0.4.0", | ||||
|     "c8": "^8.0.0", | ||||
|     "consola": "^3.0.2", | ||||
|     "eslint": "^8.47.0", | ||||
|     "jsdom": "^22.0.0", | ||||
|     "less": "^4.1.3", | ||||
|     "prettier": "^3.0.0", | ||||
|     "typescript": "~5.2.0", | ||||
|     "unocss": "^0.55.0", | ||||
|     "c8": "^8.0.1", | ||||
|     "consola": "^3.2.3", | ||||
|     "eslint": "^8.48.0", | ||||
|     "jsdom": "^22.1.0", | ||||
|     "less": "^4.2.0", | ||||
|     "prettier": "^3.0.2", | ||||
|     "typescript": "~5.2.2", | ||||
|     "unocss": "^0.55.3", | ||||
|     "unocss-preset-scrollbar": "^0.2.1", | ||||
|     "unplugin-icons": "^0.17.0", | ||||
|     "unplugin-vue-components": "^0.25.0", | ||||
|     "unplugin-icons": "^0.16.5", | ||||
|     "unplugin-vue-components": "^0.25.1", | ||||
|     "vite": "^4.4.9", | ||||
|     "vite-plugin-pwa": "^0.16.0", | ||||
|     "vite-plugin-vue-markdown": "^0.23.5", | ||||
|     "vite-plugin-pwa": "^0.16.4", | ||||
|     "vite-plugin-vue-markdown": "^0.23.8", | ||||
|     "vite-svg-loader": "^4.0.0", | ||||
|     "vitest": "^0.34.0", | ||||
|     "vitest": "^0.34.3", | ||||
|     "workbox-window": "^7.0.0", | ||||
|     "zx": "^7.2.1" | ||||
|     "zx": "^7.2.3" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { defineConfig, devices } from '@playwright/test'; | ||||
|  | ||||
| const isCI = !!process.env.CI; | ||||
| const baseUrl = process.env.BASE_URL || 'http://localhost:5050'; | ||||
| const useWebServer = process.env.NO_WEB_SERVER !== 'true'; | ||||
|  | ||||
| /** | ||||
|  * See https://playwright.dev/docs/test-configuration. | ||||
| @@ -53,13 +52,13 @@ export default defineConfig({ | ||||
|  | ||||
|   /* Run your local dev server before starting the tests */ | ||||
|  | ||||
|   ...(useWebServer | ||||
|     && { | ||||
|       webServer: { | ||||
|         command: 'npm run preview', | ||||
|         url: 'http://127.0.0.1:5050', | ||||
|         reuseExistingServer: !isCI, | ||||
|       }, | ||||
|     } | ||||
|   ), | ||||
|   ...(isCI | ||||
|     ? {} | ||||
|     : { | ||||
|         webServer: { | ||||
|           command: 'npm run preview', | ||||
|           url: 'http://127.0.0.1:5050', | ||||
|           reuseExistingServer: true, | ||||
|         }, | ||||
|       }), | ||||
| }); | ||||
|   | ||||
							
								
								
									
										3553
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3553
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,16 +1,19 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useThemeVars } from 'naive-ui'; | ||||
| import FavoriteButton from './FavoriteButton.vue'; | ||||
| import { useAppTheme } from '@/ui/theme/themes'; | ||||
| import type { Tool } from '@/tools/tools.types'; | ||||
|  | ||||
| const props = defineProps<{ tool: Tool & { category: string } }>(); | ||||
| const { tool } = toRefs(props); | ||||
| const theme = useThemeVars(); | ||||
|  | ||||
| const appTheme = useAppTheme(); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <router-link :to="tool.path"> | ||||
|     <c-card class="tool-card" shadow> | ||||
|     <c-card class="tool-card"> | ||||
|       <div flex items-center justify-between> | ||||
|         <n-icon class="icon" size="40" :component="tool.icon" /> | ||||
|         <div flex items-center gap-8px> | ||||
| @@ -29,14 +32,15 @@ const theme = useThemeVars(); | ||||
|           <FavoriteButton :tool="tool" /> | ||||
|         </div> | ||||
|       </div> | ||||
|       <n-h3 class="title" truncate> | ||||
|         {{ tool.name }} | ||||
|       <n-h3 class="title"> | ||||
|         <n-ellipsis>{{ tool.name }}</n-ellipsis> | ||||
|       </n-h3> | ||||
|  | ||||
|       <div class="description"> | ||||
|         <div line-clamp-2 style="min-height: 44.78px"> | ||||
|         <n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px"> | ||||
|           {{ tool.description }} | ||||
|         </div> | ||||
|           <br>  | ||||
|         </n-ellipsis> | ||||
|       </div> | ||||
|     </c-card> | ||||
|   </router-link> | ||||
| @@ -48,14 +52,16 @@ a { | ||||
| } | ||||
|  | ||||
| .tool-card { | ||||
|   transition: border-color ease 0.5s; | ||||
|   border-width: 2px !important; | ||||
|   color: transparent; | ||||
|   position: relative; | ||||
|   border-radius: 15px; | ||||
|   border: none; | ||||
|  | ||||
|   &:hover { | ||||
|     border-color: v-bind('appTheme.primary.colorHover'); | ||||
|   } | ||||
|  | ||||
|   .icon { | ||||
|     opacity: 0.4; | ||||
|     opacity: 0.6; | ||||
|     color: v-bind('theme.textColorBase'); | ||||
|   } | ||||
|  | ||||
| @@ -68,46 +74,5 @@ a { | ||||
|     color: v-bind('theme.textColorBase'); | ||||
|     margin: 5px 0; | ||||
|   } | ||||
|  | ||||
|   &::after { | ||||
|     --mask-radius: 20em; | ||||
|  | ||||
|     border-radius: 15px; | ||||
|     content: ''; | ||||
|     position: absolute; | ||||
|     inset: 0; | ||||
|     pointer-events: none; | ||||
|     user-select: none; | ||||
|     display: block; | ||||
|     height: calc(100% - 4px) ; | ||||
|     width:  calc(100% - 4px) ; | ||||
|     background: #18a05818; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     opacity: 1; | ||||
|     border: 2px solid transparent; | ||||
|     transition: all 0.2s ease-in-out; | ||||
|  | ||||
|     -webkit-mask: radial-gradient( | ||||
|       var(--mask-radius) var(--mask-radius) at 45px 45px, | ||||
|       #000 1%, | ||||
|       transparent 50% | ||||
|     ); | ||||
|  | ||||
|     mask: radial-gradient( | ||||
|       var(--mask-radius) var(--mask-radius) at 45px 45px, | ||||
|       #000 1%, | ||||
|       transparent 50% | ||||
|     ); | ||||
|  | ||||
|     will-change: mask; | ||||
|   } | ||||
|  | ||||
|   &:hover { | ||||
|     &::after { | ||||
|       --mask-radius: 50em; | ||||
|       border: 2px solid #18a058; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,39 +1,99 @@ | ||||
| <script setup lang="ts"> | ||||
| import { Heart } from '@vicons/tabler'; | ||||
| import { useHead } from '@vueuse/head'; | ||||
| import ColoredCard from '../components/ColoredCard.vue'; | ||||
| import ToolCard from '../components/ToolCard.vue'; | ||||
| import { useToolStore } from '@/tools/tools.store'; | ||||
| import { config } from '@/config'; | ||||
|  | ||||
| const toolStore = useToolStore(); | ||||
|  | ||||
| useHead({ title: 'IT Tools - Handy online tools for developers' }); | ||||
| const { t } = useI18n(); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div class="home-page" m-auto mt-50px max-w-1800px> | ||||
|     <div my-8 /> | ||||
|   <div class="home-page"> | ||||
|     <div class="grid-wrapper"> | ||||
|       <n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> | ||||
|         <n-gi> | ||||
|           <ColoredCard title="You like it-tools?" :icon="Heart"> | ||||
|             Give us a star on | ||||
|             <a | ||||
|               href="https://github.com/CorentinTh/it-tools" | ||||
|               rel="noopener" | ||||
|               target="_blank" | ||||
|               aria-label="IT-Tools' GitHub repository" | ||||
|             >GitHub</a> | ||||
|             or follow us on | ||||
|             <a | ||||
|               href="https://twitter.com/ittoolsdottech" | ||||
|               rel="noopener" | ||||
|               target="_blank" | ||||
|               aria-label="IT-Tools' Twitter account" | ||||
|             >Twitter</a>! Thank you | ||||
|             <n-icon :component="Heart" /> | ||||
|           </ColoredCard> | ||||
|         </n-gi> | ||||
|       </n-grid> | ||||
|  | ||||
|     <div v-if="toolStore.favoriteTools.length > 0"> | ||||
|       <div mb-2 mt-6 text-lg font-semibold> | ||||
|         {{ $t('home.categories.yourFavoriteTools') }} | ||||
|       </div> | ||||
|       <div grid-cols="1 sm:2 md:2 lg:3 xl:4" grid gap-12px> | ||||
|         <tool-card v-for="tool in toolStore.favoriteTools" :key="tool.name" :tool="tool" /> | ||||
|       </div> | ||||
|     </div> | ||||
|       <transition name="height"> | ||||
|         <div v-if="toolStore.favoriteTools.length > 0"> | ||||
|           <n-h3>Your favorite tools</n-h3> | ||||
|           <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> | ||||
|             <n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name"> | ||||
|               <ToolCard :tool="tool" /> | ||||
|             </n-gi> | ||||
|           </n-grid> | ||||
|         </div> | ||||
|       </transition> | ||||
|  | ||||
|     <div v-if="toolStore.newTools.length > 0"> | ||||
|       <div mb-2 mt-6 text-lg font-semibold> | ||||
|         {{ $t('home.categories.newestTools') }} | ||||
|       <div v-if="toolStore.newTools.length > 0"> | ||||
|         <n-h3>{{ t('home.categories.newestTools', 'Newest tools') }}</n-h3> | ||||
|         <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> | ||||
|           <n-gi v-for="tool in toolStore.newTools" :key="tool.name"> | ||||
|             <ToolCard :tool="tool" /> | ||||
|           </n-gi> | ||||
|         </n-grid> | ||||
|       </div> | ||||
|       <div grid-cols="1 sm:2 md:2 lg:3 xl:4" grid gap-12px> | ||||
|         <tool-card v-for="tool in toolStore.newTools" :key="tool.name" :tool="tool" /> | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|     <div mb-2 mt-6 text-lg font-semibold> | ||||
|       {{ $t('home.categories.allTheTools') }} | ||||
|     </div> | ||||
|     <div grid-cols="1 sm:2 md:2 lg:3 xl:4" grid gap-12px> | ||||
|       <tool-card v-for="tool in toolStore.tools" :key="tool.name" :tool="tool" /> | ||||
|       <n-h3>All the tools</n-h3> | ||||
|       <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> | ||||
|         <n-gi v-for="tool in toolStore.tools" :key="tool.name"> | ||||
|           <transition> | ||||
|             <ToolCard :tool="tool" /> | ||||
|           </transition> | ||||
|         </n-gi> | ||||
|       </n-grid> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style scoped lang="less"> | ||||
| .home-page { | ||||
|   padding-top: 50px; | ||||
| } | ||||
|  | ||||
| .n-h3 { | ||||
|   margin-bottom: 10px; | ||||
| } | ||||
|  | ||||
| ::v-deep(.n-grid) { | ||||
|   margin-bottom: 30px; | ||||
| } | ||||
|  | ||||
| .height-enter-active, | ||||
| .height-leave-active { | ||||
|   transition: all 0.5s ease-in-out; | ||||
|   overflow: hidden; | ||||
|   max-height: 500px; | ||||
| } | ||||
|  | ||||
| .height-enter-from, | ||||
| .height-leave-to { | ||||
|   max-height: 42px; | ||||
|   overflow: hidden; | ||||
|   opacity: 0; | ||||
|   margin-bottom: 0; | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,22 +1,6 @@ | ||||
| import type { Plugin } from 'vue'; | ||||
| import type { App } from 'vue'; | ||||
| import { createI18n } from 'vue-i18n'; | ||||
| import baseMessages from '@intlify/unplugin-vue-i18n/messages'; | ||||
| import _ from 'lodash'; | ||||
| import { parse as parseYaml } from 'yaml'; | ||||
|  | ||||
| const i18nFiles = import.meta.glob('../tools/*/locales/**.yml', { as: 'raw' }); | ||||
|  | ||||
| const messagesByTools = await Promise.all(_.map(i18nFiles, async (fileDescriptor, path) => { | ||||
|   const [, locale] = path.match(/\.\/tools\/.*?\/locales\/(.*)\.ya?ml$/i) ?? []; | ||||
|   const content = parseYaml(await fileDescriptor()); | ||||
|  | ||||
|   return { [locale]: content }; | ||||
| })); | ||||
|  | ||||
| const messages = _.merge( | ||||
|   baseMessages, | ||||
|   _.merge({}, ...messagesByTools), | ||||
| ); | ||||
| import messages from '@intlify/unplugin-vue-i18n/messages'; | ||||
|  | ||||
| const i18n = createI18n({ | ||||
|   legacy: false, | ||||
| @@ -24,8 +8,8 @@ const i18n = createI18n({ | ||||
|   messages, | ||||
| }); | ||||
|  | ||||
| export const i18nPlugin: Plugin = { | ||||
|   install: (app) => { | ||||
| export const i18nPlugin = { | ||||
|   install: (app: App) => { | ||||
|     app.use(i18n); | ||||
|   }, | ||||
| }; | ||||
|   | ||||
| @@ -45,7 +45,7 @@ const compareMatch = computed(() => compareSync(compareString.value, compareHash | ||||
|         <c-input-text v-model:value="compareString" placeholder="Your string to compare..." raw-text /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Your hash: " label-placement="left"> | ||||
|         <c-input-text v-model:value="compareHash" placeholder="Your hash to compare..." raw-text /> | ||||
|         <c-input-text v-model:value="compareHash" placeholder="Your hahs to compare..." raw-text /> | ||||
|       </n-form-item> | ||||
|       <n-form-item label="Do they match ? " label-placement="left" :show-feedback="false"> | ||||
|         <div class="compare-result" :class="{ positive: compareMatch }"> | ||||
|   | ||||
| @@ -146,7 +146,7 @@ function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date | ||||
|       <c-input-text | ||||
|         v-model:value="inputDate" | ||||
|         autofocus | ||||
|         placeholder="Put your date string here..." | ||||
|         placeholder="Put you date string here..." | ||||
|         clearable | ||||
|         test-id="date-time-converter-input" | ||||
|         :validation="validation" | ||||
|   | ||||
| @@ -1,52 +0,0 @@ | ||||
| import { type Page, expect, test } from '@playwright/test'; | ||||
|  | ||||
| async function extractIbanInfo({ page }: { page: Page }) { | ||||
|   const itemsLines = await page | ||||
|     .locator('.c-key-value-list__item').all(); | ||||
|  | ||||
|   return await Promise.all( | ||||
|     itemsLines.map(async item => [ | ||||
|       (await item.locator('.c-key-value-list__key').textContent() ?? '').trim(), | ||||
|       (await item.locator('.c-key-value-list__value').textContent() ?? '').trim(), | ||||
|     ]), | ||||
|   ); | ||||
| } | ||||
|  | ||||
| test.describe('Tool - Iban validator and parser', () => { | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('/iban-validator-and-parser'); | ||||
|   }); | ||||
|  | ||||
|   test('Has correct title', async ({ page }) => { | ||||
|     await expect(page).toHaveTitle('IBAN validator and parser - IT Tools'); | ||||
|   }); | ||||
|  | ||||
|   test('iban info are extracted from a valid iban', async ({ page }) => { | ||||
|     await page.getByTestId('iban-input').fill('DE89370400440532013000'); | ||||
|  | ||||
|     const ibanInfo = await extractIbanInfo({ page }); | ||||
|  | ||||
|     expect(ibanInfo).toEqual([ | ||||
|       ['Is IBAN valid ?', 'Yes'], | ||||
|       ['Is IBAN a QR-IBAN ?', 'No'], | ||||
|       ['Country code', 'DE'], | ||||
|       ['BBAN', '370400440532013000'], | ||||
|       ['IBAN friendly format', 'DE89 3704 0044 0532 0130 00'], | ||||
|     ]); | ||||
|   }); | ||||
|  | ||||
|   test('invalid iban errors are displayed', async ({ page }) => { | ||||
|     await page.getByTestId('iban-input').fill('FR7630006060011234567890189'); | ||||
|  | ||||
|     const ibanInfo = await extractIbanInfo({ page }); | ||||
|  | ||||
|     expect(ibanInfo).toEqual([ | ||||
|       ['Is IBAN valid ?', 'No'], | ||||
|       ['IBAN errors', 'Wrong account bank branch checksum Wrong IBAN checksum'], | ||||
|       ['Is IBAN a QR-IBAN ?', 'No'], | ||||
|       ['Country code', 'N/A'], | ||||
|       ['BBAN', 'N/A'], | ||||
|       ['IBAN friendly format', 'FR76 3000 6060 0112 3456 7890 189'], | ||||
|     ]); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,18 +0,0 @@ | ||||
| import { ValidationErrorsIBAN } from 'ibantools'; | ||||
|  | ||||
| export { getFriendlyErrors }; | ||||
|  | ||||
| const ibanErrorToMessage = { | ||||
|   [ValidationErrorsIBAN.NoIBANProvided]: 'No IBAN provided', | ||||
|   [ValidationErrorsIBAN.NoIBANCountry]: 'No IBAN country', | ||||
|   [ValidationErrorsIBAN.WrongBBANLength]: 'Wrong BBAN length', | ||||
|   [ValidationErrorsIBAN.WrongBBANFormat]: 'Wrong BBAN format', | ||||
|   [ValidationErrorsIBAN.ChecksumNotNumber]: 'Checksum is not a number', | ||||
|   [ValidationErrorsIBAN.WrongIBANChecksum]: 'Wrong IBAN checksum', | ||||
|   [ValidationErrorsIBAN.WrongAccountBankBranchChecksum]: 'Wrong account bank branch checksum', | ||||
|   [ValidationErrorsIBAN.QRIBANNotAllowed]: 'QR-IBAN not allowed', | ||||
| }; | ||||
|  | ||||
| function getFriendlyErrors(errorCodes: ValidationErrorsIBAN[]) { | ||||
|   return errorCodes.map(errorCode => ibanErrorToMessage[errorCode]).filter(Boolean); | ||||
| } | ||||
| @@ -1,71 +0,0 @@ | ||||
| <script setup lang="ts"> | ||||
| import { extractIBAN, friendlyFormatIBAN, isQRIBAN, validateIBAN } from 'ibantools'; | ||||
| import { getFriendlyErrors } from './iban-validator-and-parser.service'; | ||||
| import type { CKeyValueListItems } from '@/ui/c-key-value-list/c-key-value-list.types'; | ||||
|  | ||||
| const rawIban = ref(''); | ||||
|  | ||||
| const ibanInfo = computed<CKeyValueListItems>(() => { | ||||
|   const iban = rawIban.value.toUpperCase().replace(/\s/g, '').replace(/-/g, ''); | ||||
|  | ||||
|   if (iban === '') { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   const { valid: isIbanValid, errorCodes } = validateIBAN(iban); | ||||
|   const { countryCode, bban } = extractIBAN(iban); | ||||
|   const errors = getFriendlyErrors(errorCodes); | ||||
|  | ||||
|   return [ | ||||
|  | ||||
|     { | ||||
|       label: 'Is IBAN valid ?', | ||||
|       value: isIbanValid, | ||||
|       showCopyButton: false, | ||||
|     }, | ||||
|     { | ||||
|       label: 'IBAN errors', | ||||
|       value: errors.length === 0 ? undefined : errors, | ||||
|       hideOnNil: true, | ||||
|       showCopyButton: false, | ||||
|     }, | ||||
|     { | ||||
|       label: 'Is IBAN a QR-IBAN ?', | ||||
|       value: isQRIBAN(iban), | ||||
|       showCopyButton: false, | ||||
|     }, | ||||
|     { | ||||
|       label: 'Country code', | ||||
|       value: countryCode, | ||||
|     }, | ||||
|     { | ||||
|       label: 'BBAN', | ||||
|       value: bban, | ||||
|     }, | ||||
|     { | ||||
|       label: 'IBAN friendly format', | ||||
|       value: friendlyFormatIBAN(iban), | ||||
|     }, | ||||
|   ]; | ||||
| }); | ||||
|  | ||||
| const ibanExamples = [ | ||||
|   'FR7630006000011234567890189', | ||||
|   'DE89370400440532013000', | ||||
|   'GB29NWBK60161331926819', | ||||
| ]; | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <c-input-text v-model:value="rawIban" placeholder="Enter an IBAN to check for validity..." test-id="iban-input" /> | ||||
|  | ||||
|     <c-key-value-list :items="ibanInfo" my-5 data-test-id="iban-info" /> | ||||
|  | ||||
|     <c-card title="Valid IBAN examples"> | ||||
|       <div v-for="iban in ibanExamples" :key="iban"> | ||||
|         <c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" /> | ||||
|       </div> | ||||
|     </c-card> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -1,12 +0,0 @@ | ||||
| import { defineTool } from '../tool'; | ||||
| import Bank from '~icons/mdi/bank'; | ||||
|  | ||||
| export const tool = defineTool({ | ||||
|   name: 'IBAN validator and parser', | ||||
|   path: '/iban-validator-and-parser', | ||||
|   description: 'Validate and parse IBAN numbers. Check if IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format.', | ||||
|   keywords: ['iban', 'validator', 'and', 'parser', 'bic', 'bank'], | ||||
|   component: () => import('./iban-validator-and-parser.vue'), | ||||
|   icon: Bank, | ||||
|   createdAt: new Date('2023-08-26'), | ||||
| }); | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { tool as base64FileConverter } from './base64-file-converter'; | ||||
| import { tool as base64StringConverter } from './base64-string-converter'; | ||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | ||||
| import { tool as ibanValidatorAndParser } from './iban-validator-and-parser'; | ||||
| import { tool as stringObfuscator } from './string-obfuscator'; | ||||
| import { tool as textDiff } from './text-diff'; | ||||
| import { tool as emojiPicker } from './emoji-picker'; | ||||
| @@ -56,7 +55,6 @@ import { tool as metaTagGenerator } from './meta-tag-generator'; | ||||
| import { tool as mimeTypes } from './mime-types'; | ||||
| import { tool as otpCodeGeneratorAndValidator } from './otp-code-generator-and-validator'; | ||||
| import { tool as qrCodeGenerator } from './qr-code-generator'; | ||||
| import { tool as wifiQrCodeGenerator } from './wifi-qr-code-generator'; | ||||
| import { tool as randomPortGenerator } from './random-port-generator'; | ||||
| import { tool as romanNumeralConverter } from './roman-numeral-converter'; | ||||
| import { tool as sqlPrettify } from './sql-prettify'; | ||||
| @@ -118,7 +116,7 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|   }, | ||||
|   { | ||||
|     name: 'Images and videos', | ||||
|     components: [qrCodeGenerator, wifiQrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], | ||||
|     components: [qrCodeGenerator, svgPlaceholderGenerator, cameraRecorder], | ||||
|   }, | ||||
|   { | ||||
|     name: 'Development', | ||||
| @@ -153,7 +151,7 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|   }, | ||||
|   { | ||||
|     name: 'Data', | ||||
|     components: [phoneParserAndFormatter, ibanValidatorAndParser], | ||||
|     components: [phoneParserAndFormatter], | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
|   | ||||
| @@ -14,6 +14,6 @@ test.describe('Tool - Password strength analyser', () => { | ||||
|  | ||||
|     const crackDuration = await page.getByTestId('crack-duration').textContent(); | ||||
|  | ||||
|     expect(crackDuration).toEqual('15,091 millennia, 3 centuries'); | ||||
|     expect(crackDuration).toEqual('15,091 milleniums, 3 centurys'); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -19,20 +19,20 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) { | ||||
|   } | ||||
|  | ||||
|   const timeUnits = [ | ||||
|     { unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation, plural: 'millennia' }, | ||||
|     { unit: 'century', secondsInUnit: 3153600000, plural: 'centuries' }, | ||||
|     { unit: 'decade', secondsInUnit: 315360000, plural: 'decades' }, | ||||
|     { unit: 'year', secondsInUnit: 31536000, plural: 'years' }, | ||||
|     { unit: 'month', secondsInUnit: 2592000, plural: 'months' }, | ||||
|     { unit: 'week', secondsInUnit: 604800, plural: 'weeks' }, | ||||
|     { unit: 'day', secondsInUnit: 86400, plural: 'days' }, | ||||
|     { unit: 'hour', secondsInUnit: 3600, plural: 'hours' }, | ||||
|     { unit: 'minute', secondsInUnit: 60, plural: 'minutes' }, | ||||
|     { unit: 'second', secondsInUnit: 1, plural: 'seconds' }, | ||||
|     { unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation }, | ||||
|     { unit: 'century', secondsInUnit: 3153600000 }, | ||||
|     { unit: 'decade', secondsInUnit: 315360000 }, | ||||
|     { unit: 'year', secondsInUnit: 31536000 }, | ||||
|     { unit: 'month', secondsInUnit: 2592000 }, | ||||
|     { unit: 'week', secondsInUnit: 604800 }, | ||||
|     { unit: 'day', secondsInUnit: 86400 }, | ||||
|     { unit: 'hour', secondsInUnit: 3600 }, | ||||
|     { unit: 'minute', secondsInUnit: 60 }, | ||||
|     { unit: 'second', secondsInUnit: 1 }, | ||||
|   ]; | ||||
|  | ||||
|   return _.chain(timeUnits) | ||||
|     .map(({ unit, secondsInUnit, plural, format = _.identity }) => { | ||||
|     .map(({ unit, secondsInUnit, format = _.identity }) => { | ||||
|       const quantity = Math.floor(seconds / secondsInUnit); | ||||
|       seconds %= secondsInUnit; | ||||
|  | ||||
| @@ -41,7 +41,7 @@ function getHumanFriendlyDuration({ seconds }: { seconds: number }) { | ||||
|       } | ||||
|  | ||||
|       const formattedQuantity = format(quantity); | ||||
|       return `${formattedQuantity} ${quantity > 1 ? plural : unit}`; | ||||
|       return `${formattedQuantity} ${unit}${quantity > 1 ? 's' : ''}`; | ||||
|     }) | ||||
|     .compact() | ||||
|     .take(2) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ export const tool = defineTool({ | ||||
|   name: 'UUIDs v4 generator', | ||||
|   path: '/uuid-generator', | ||||
|   description: | ||||
|     'A Universally Unique Identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot!).', | ||||
|     'A universally unique identifier (UUID) is a 128-bit number used to identify information in computer systems. The number of possible UUIDs is 16^32, which is 2^128 or about 3.4x10^38 (which is a lot !).', | ||||
|   keywords: ['uuid', 'v4', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'], | ||||
|   component: () => import('./uuid-generator.vue'), | ||||
|   icon: Fingerprint, | ||||
|   | ||||
| @@ -1,13 +0,0 @@ | ||||
| import { Qrcode } from '@vicons/tabler'; | ||||
| import { defineTool } from '../tool'; | ||||
|  | ||||
| export const tool = defineTool({ | ||||
|   name: 'WiFi QR Code generator', | ||||
|   path: '/wifi-qrcode-generator', | ||||
|   description: | ||||
|     'Generate and download QR-codes for quick connections to WiFi networks.', | ||||
|   keywords: ['qr', 'code', 'generator', 'square', 'color', 'link', 'low', 'medium', 'quartile', 'high', 'transparent', 'wifi'], | ||||
|   component: () => import('./wifi-qr-code-generator.vue'), | ||||
|   icon: Qrcode, | ||||
|   createdAt: new Date('2023-09-06'), | ||||
| }); | ||||
| @@ -1,146 +0,0 @@ | ||||
| import { type MaybeRef, get } from '@vueuse/core'; | ||||
| import QRCode, { type QRCodeToDataURLOptions } from 'qrcode'; | ||||
| import { isRef, ref, watch } from 'vue'; | ||||
|  | ||||
| export const wifiEncryptions = ['WEP', 'WPA', 'nopass', 'WPA2-EAP'] as const; | ||||
| export type WifiEncryption = typeof wifiEncryptions[number]; | ||||
|  | ||||
| // @see https://en.wikipedia.org/wiki/Extensible_Authentication_Protocol | ||||
| // for a list of available EAP methods. There are a lot (40!) of them. | ||||
| export const EAPMethods = [ | ||||
|   'MD5', | ||||
|   'POTP', | ||||
|   'GTC', | ||||
|   'TLS', | ||||
|   'IKEv2', | ||||
|   'SIM', | ||||
|   'AKA', | ||||
|   'AKA\'', | ||||
|   'TTLS', | ||||
|   'PWD', | ||||
|   'LEAP', | ||||
|   'PSK', | ||||
|   'FAST', | ||||
|   'TEAP', | ||||
|   'EKE', | ||||
|   'NOOB', | ||||
|   'PEAP', | ||||
| ] as const; | ||||
| export type EAPMethod = typeof EAPMethods[number]; | ||||
|  | ||||
| export const EAPPhase2Methods = [ | ||||
|   'None', | ||||
|   'MSCHAPV2', | ||||
| ] as const; | ||||
| export type EAPPhase2Method = typeof EAPPhase2Methods[number]; | ||||
|  | ||||
| interface IWifiQRCodeOptions { | ||||
|   ssid: MaybeRef<string> | ||||
|   password: MaybeRef<string> | ||||
|   eapMethod: MaybeRef<EAPMethod> | ||||
|   isHiddenSSID: MaybeRef<boolean> | ||||
|   eapAnonymous: MaybeRef<boolean> | ||||
|   eapIdentity: MaybeRef<string> | ||||
|   eapPhase2Method: MaybeRef<EAPPhase2Method> | ||||
|   color: { foreground: MaybeRef<string>; background: MaybeRef<string> } | ||||
|   options?: QRCodeToDataURLOptions | ||||
| } | ||||
|  | ||||
| interface GetQrCodeTextOptions { | ||||
|   ssid: string | ||||
|   password: string | ||||
|   encryption: WifiEncryption | ||||
|   eapMethod: EAPMethod | ||||
|   isHiddenSSID: boolean | ||||
|   eapAnonymous: boolean | ||||
|   eapIdentity: string | ||||
|   eapPhase2Method: EAPPhase2Method | ||||
| } | ||||
|  | ||||
| function escapeString(str: string) { | ||||
|   // replaces \, ;, ,, " and : with the same character preceded by a backslash | ||||
|   return str.replace(/([\\;,:"])/g, '\\$1'); | ||||
| } | ||||
|  | ||||
| function getQrCodeText(options: GetQrCodeTextOptions): string | null { | ||||
|   const { ssid, password, encryption, eapMethod, isHiddenSSID, eapAnonymous, eapIdentity, eapPhase2Method } = options; | ||||
|   if (!ssid) { | ||||
|     return null; | ||||
|   } | ||||
|   if (encryption === 'nopass') { | ||||
|     return `WIFI:S:${escapeString(ssid)};;`; // type can be omitted in that case, and password is not needed, makes the QR Code smaller | ||||
|   } | ||||
|   if (encryption !== 'WPA2-EAP' && password) { | ||||
|     // EAP has a lot of options, so we'll handle it separately | ||||
|     // WPA and WEP are pretty simple though. | ||||
|     return `WIFI:S:${escapeString(ssid)};T:${encryption};P:${escapeString(password)};${isHiddenSSID ? 'H:true' : ''};`; | ||||
|   } | ||||
|   if (encryption === 'WPA2-EAP' && password && eapMethod) { | ||||
|     // WPA2-EAP string is a lot more complex, first off, we drop the text if there is no identity, and it's not anonymous. | ||||
|     if (!eapIdentity && !eapAnonymous) { | ||||
|       return null; | ||||
|     } | ||||
|     // From reading, I could only find that a phase 2 is required for the PEAP method, I may be wrong though, I didn't read the whole spec. | ||||
|     if (eapMethod === 'PEAP' && !eapPhase2Method) { | ||||
|       return null; | ||||
|     } | ||||
|     // The string is built in the following order: | ||||
|     // 1. SSID | ||||
|     // 2. Authentication type | ||||
|     // 3. Password | ||||
|     // 4. EAP method | ||||
|     // 5. EAP phase 2 method | ||||
|     // 6. Identity or anonymous if checked | ||||
|     // 7. Hidden SSID if checked | ||||
|     const identity = eapAnonymous ? 'A:anon' : `I:${escapeString(eapIdentity)}`; | ||||
|     const phase2 = eapPhase2Method !== 'None' ? `PH2:${eapPhase2Method};` : ''; | ||||
|     return `WIFI:S:${escapeString(ssid)};T:WPA2-EAP;P:${escapeString(password)};E:${eapMethod};${phase2}${identity};${isHiddenSSID ? 'H:true' : ''};`; | ||||
|   } | ||||
|   return null; | ||||
| } | ||||
|  | ||||
| export function useWifiQRCode({ | ||||
|   ssid, | ||||
|   password, | ||||
|   eapMethod, | ||||
|   isHiddenSSID, | ||||
|   eapAnonymous, | ||||
|   eapIdentity, | ||||
|   eapPhase2Method, | ||||
|   color: { background, foreground }, | ||||
|   options, | ||||
| }: IWifiQRCodeOptions) { | ||||
|   const qrcode = ref(''); | ||||
|   const encryption = ref<WifiEncryption>('WPA'); | ||||
|  | ||||
|   watch( | ||||
|     [ssid, password, encryption, eapMethod, isHiddenSSID, eapAnonymous, eapIdentity, eapPhase2Method, background, foreground].filter(isRef), | ||||
|     async () => { | ||||
|       // @see https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11 | ||||
|       // This is the full spec, there's quite a bit of logic to generate the string embeddedin the QR code. | ||||
|       const text = getQrCodeText({ | ||||
|         ssid: get(ssid), | ||||
|         password: get(password), | ||||
|         encryption: get(encryption), | ||||
|         eapMethod: get(eapMethod), | ||||
|         isHiddenSSID: get(isHiddenSSID), | ||||
|         eapAnonymous: get(eapAnonymous), | ||||
|         eapIdentity: get(eapIdentity), | ||||
|         eapPhase2Method: get(eapPhase2Method), | ||||
|       }); | ||||
|       if (text) { | ||||
|         qrcode.value = await QRCode.toDataURL(get(text).trim(), { | ||||
|           color: { | ||||
|             dark: get(foreground), | ||||
|             light: get(background), | ||||
|             ...options?.color, | ||||
|           }, | ||||
|           errorCorrectionLevel: 'M', | ||||
|           ...options, | ||||
|         }); | ||||
|       } | ||||
|     }, | ||||
|     { immediate: true }, | ||||
|   ); | ||||
|   return { qrcode, encryption }; | ||||
| } | ||||
| @@ -1,153 +0,0 @@ | ||||
| <script setup lang="ts"> | ||||
| import { | ||||
|   EAPMethods, | ||||
|   EAPPhase2Methods, | ||||
|   useWifiQRCode, | ||||
| } from './useQRCode'; | ||||
| import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||||
|  | ||||
| const foreground = ref('#000000ff'); | ||||
| const background = ref('#ffffffff'); | ||||
|  | ||||
| const ssid = ref(); | ||||
| const password = ref(); | ||||
| const eapMethod = ref(); | ||||
| const isHiddenSSID = ref(false); | ||||
| const eapAnonymous = ref(false); | ||||
| const eapIdentity = ref(); | ||||
| const eapPhase2Method = ref(); | ||||
|  | ||||
| const { qrcode, encryption } = useWifiQRCode({ | ||||
|   ssid, | ||||
|   password, | ||||
|   eapMethod, | ||||
|   isHiddenSSID, | ||||
|   eapAnonymous, | ||||
|   eapIdentity, | ||||
|   eapPhase2Method, | ||||
|   color: { | ||||
|     background, | ||||
|     foreground, | ||||
|   }, | ||||
|   options: { width: 1024 }, | ||||
| }); | ||||
|  | ||||
| const { download } = useDownloadFileFromBase64({ source: qrcode, filename: 'qr-code.png' }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-card> | ||||
|     <div grid grid-cols-1 gap-12> | ||||
|       <div> | ||||
|         <c-select | ||||
|           v-model:value="encryption" | ||||
|           mb-4 | ||||
|           label="Encryption method" | ||||
|           default-value="WPA" | ||||
|           label-position="left" | ||||
|           label-width="130px" | ||||
|           label-align="right" | ||||
|           :options="[ | ||||
|             { | ||||
|               label: 'No password', | ||||
|               value: 'nopass', | ||||
|             }, | ||||
|             { | ||||
|               label: 'WPA/WPA2', | ||||
|               value: 'WPA', | ||||
|             }, | ||||
|             { | ||||
|               label: 'WEP', | ||||
|               value: 'WEP', | ||||
|             }, | ||||
|             { | ||||
|               label: 'WPA2-EAP', | ||||
|               value: 'WPA2-EAP', | ||||
|             }, | ||||
|           ]" | ||||
|         /> | ||||
|         <div class="mb-6 flex flex-row items-center gap-2"> | ||||
|           <c-input-text | ||||
|             v-model:value="ssid" | ||||
|             label-position="left" | ||||
|             label-width="130px" | ||||
|             label-align="right" | ||||
|             label="SSID:" | ||||
|             rows="1" | ||||
|             autosize | ||||
|             placeholder="Your WiFi SSID..." | ||||
|             mb-6 | ||||
|           /> | ||||
|           <n-checkbox v-model:checked="isHiddenSSID"> | ||||
|             Hidden SSID | ||||
|           </n-checkbox> | ||||
|         </div> | ||||
|         <c-input-text | ||||
|           v-if="encryption !== 'nopass'" | ||||
|           v-model:value="password" | ||||
|           label-position="left" | ||||
|           label-width="130px" | ||||
|           label-align="right" | ||||
|           label="Password:" | ||||
|           rows="1" | ||||
|           autosize | ||||
|           type="password" | ||||
|           placeholder="Your WiFi Password..." | ||||
|           mb-6 | ||||
|         /> | ||||
|         <c-select | ||||
|           v-if="encryption === 'WPA2-EAP'" | ||||
|           v-model:value="eapMethod" | ||||
|           label="EAP method" | ||||
|           label-position="left" | ||||
|           label-width="130px" | ||||
|           label-align="right" | ||||
|           :options="EAPMethods.map((method) => ({ label: method, value: method }))" | ||||
|           searchable mb-4 | ||||
|         /> | ||||
|         <div v-if="encryption === 'WPA2-EAP'" class="mb-6 flex flex-row items-center gap-2"> | ||||
|           <c-input-text | ||||
|             v-model:value="eapIdentity" | ||||
|             label-position="left" | ||||
|             label-width="130px" | ||||
|             label-align="right" | ||||
|             label="Identity:" | ||||
|             rows="1" | ||||
|             autosize | ||||
|             placeholder="Your EAP Identity..." | ||||
|             mb-6 | ||||
|           /> | ||||
|           <n-checkbox v-model:checked="eapAnonymous"> | ||||
|             Anonymous? | ||||
|           </n-checkbox> | ||||
|         </div> | ||||
|         <c-select | ||||
|           v-if="encryption === 'WPA2-EAP'" | ||||
|           v-model:value="eapPhase2Method" | ||||
|           label="EAP Phase 2 method" | ||||
|           label-position="left" | ||||
|           label-width="130px" | ||||
|           label-align="right" | ||||
|           :options="EAPPhase2Methods.map((method) => ({ label: method, value: method }))" | ||||
|           searchable mb-4 | ||||
|         /> | ||||
|         <n-form label-width="130" label-placement="left"> | ||||
|           <n-form-item label="Foreground color:"> | ||||
|             <n-color-picker v-model:value="foreground" :modes="['hex']" /> | ||||
|           </n-form-item> | ||||
|           <n-form-item label="Background color:"> | ||||
|             <n-color-picker v-model:value="background" :modes="['hex']" /> | ||||
|           </n-form-item> | ||||
|         </n-form> | ||||
|       </div> | ||||
|       <div v-if="qrcode"> | ||||
|         <div flex flex-col items-center gap-3> | ||||
|           <img alt="wifi-qrcode" :src="qrcode" width="200"> | ||||
|           <c-button @click="download"> | ||||
|             Download qr-code | ||||
|           </c-button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </c-card> | ||||
| </template> | ||||
| @@ -1,27 +0,0 @@ | ||||
| <script lang="ts" setup> | ||||
| import _ from 'lodash'; | ||||
| import type { CKeyValueListItem } from './c-key-value-list.types'; | ||||
|  | ||||
| const props = defineProps<{ item: CKeyValueListItem }>(); | ||||
| const { item } = toRefs(props); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div v-if="_.isArray(item.value)"> | ||||
|     <div v-for="value in item.value" :key="value"> | ||||
|       <c-text-copyable :value="value" :show-icon="item.showCopyButton ?? true" /> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div v-else-if="_.isBoolean(item.value)"> | ||||
|     <c-text-copyable :value="item.value ? 'true' : 'false'" :displayed-value="item.value ? 'Yes' : 'No'" :show-icon="item.showCopyButton ?? true" /> | ||||
|   </div> | ||||
|   <div v-else-if="_.isNumber(item.value)" font-mono> | ||||
|     <c-text-copyable :value="String(item.value)" :show-icon="item.showCopyButton ?? true" /> | ||||
|   </div> | ||||
|   <div v-else-if="_.isNil(item.value) || item.value === ''" op-70> | ||||
|     {{ item.placeholder ?? 'N/A' }} | ||||
|   </div> | ||||
|   <div v-else> | ||||
|     <c-text-copyable :value="item.value" :show-icon="item.showCopyButton ?? true" /> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -1,9 +0,0 @@ | ||||
| export interface CKeyValueListItem { | ||||
|   label: string | ||||
|   value: string | string[] | number | boolean | undefined | null | ||||
|   hideOnNil?: boolean | ||||
|   placeholder?: string | ||||
|   showCopyButton?: boolean | ||||
| } | ||||
|  | ||||
| export type CKeyValueListItems = CKeyValueListItem[]; | ||||
| @@ -1,21 +0,0 @@ | ||||
| <script lang="ts" setup> | ||||
| import _ from 'lodash'; | ||||
| import type { CKeyValueListItems } from './c-key-value-list.types'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ items?: CKeyValueListItems }>(), { items: () => [] }); | ||||
| const { items } = toRefs(props); | ||||
|  | ||||
| const formattedItems = computed(() => items.value.filter(item => !_.isNil(item.value) || !item.hideOnNil)); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div my-5> | ||||
|     <div v-for="item in formattedItems" :key="item.label" flex gap-2 py-1 class="c-key-value-list__item"> | ||||
|       <div flex-basis-180px text-right font-bold class="c-key-value-list__key"> | ||||
|         {{ item.label }} | ||||
|       </div> | ||||
|  | ||||
|       <c-key-value-list-item :item="item" class="c-key-value-list__value" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -1,3 +0,0 @@ | ||||
| <template> | ||||
|   <c-text-copyable value="value" displayed-value="displayedValue" /> | ||||
| </template> | ||||
| @@ -1,17 +0,0 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
|  | ||||
| const props = withDefaults(defineProps<{ value?: string; displayedValue?: string; showIcon?: boolean }>(), { value: '', displayedValue: undefined, showIcon: true }); | ||||
| const { value, displayedValue, showIcon } = toRefs(props); | ||||
|  | ||||
| const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <c-tooltip :tooltip="isJustCopied ? 'Copied!' : 'Copy to clipboard'" cursor-pointer @click="copy"> | ||||
|     <span flex items-center gap-2> | ||||
|       {{ displayedValue ?? value }} | ||||
|       <icon-mdi-content-copy v-if="showIcon" op-40 /> | ||||
|     </span> | ||||
|   </c-tooltip> | ||||
| </template> | ||||
| @@ -1,17 +0,0 @@ | ||||
| <template> | ||||
|   <div> | ||||
|     <c-tooltip> | ||||
|       Hover me | ||||
|  | ||||
|       <template #tooltip> | ||||
|         Tooltip content | ||||
|       </template> | ||||
|     </c-tooltip> | ||||
|   </div> | ||||
|  | ||||
|   <div mt-5> | ||||
|     <c-tooltip tooltip="Tooltip content"> | ||||
|       Hover me | ||||
|     </c-tooltip> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -1,30 +0,0 @@ | ||||
| <script setup lang="ts"> | ||||
| const props = withDefaults(defineProps<{ tooltip?: string }>(), { tooltip: '' }); | ||||
| const { tooltip } = toRefs(props); | ||||
|  | ||||
| const targetRef = ref(); | ||||
| const isTargetHovered = useElementHover(targetRef); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div class="relative" inline-block> | ||||
|     <div ref="targetRef"> | ||||
|       <slot /> | ||||
|     </div> | ||||
|  | ||||
|     <div | ||||
|       class="absolute bottom-100% left-50% z-10 mb-5px whitespace-nowrap rounded bg-black px-12px py-6px text-sm text-white shadow-lg transition transition transition-duration-0.2s -translate-x-1/2" | ||||
|       :class="{ | ||||
|         'op-0 scale-0': isTargetHovered === false, | ||||
|         'op-100 scale-100': isTargetHovered, | ||||
|       }" | ||||
|     > | ||||
|       <slot | ||||
|         v-if="isTargetHovered" | ||||
|         name="tooltip" | ||||
|       > | ||||
|         {{ tooltip }} | ||||
|       </slot> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -25,7 +25,7 @@ export default defineConfig({ | ||||
|       runtimeOnly: true, | ||||
|       compositionOnly: true, | ||||
|       fullInstall: true, | ||||
|       include: [resolve(__dirname, 'locales/**')], | ||||
|       include: [resolve(__dirname, 'locales/**'), resolve(__dirname, 'src/tools/*/locales/**')], | ||||
|     }), | ||||
|     AutoImport({ | ||||
|       imports: [ | ||||
| @@ -106,7 +106,4 @@ export default defineConfig({ | ||||
|   test: { | ||||
|     exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'], | ||||
|   }, | ||||
|   build: { | ||||
|     target: 'esnext', | ||||
|   }, | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user