chore(lint): switched to a better lint config
This commit is contained in:
		
							parent
							
								
									4d2b037dbe
								
							
						
					
					
						commit
						33c9b6643f
					
				| @ -1,39 +1,14 @@ | |||||||
| /* eslint-env node */ | /** | ||||||
| require('@rushstack/eslint-patch/modern-module-resolution'); |  * @type {import('eslint').Linter.Config} | ||||||
| 
 |  */ | ||||||
| module.exports = { | module.exports = { | ||||||
|   root: true, |   root: true, | ||||||
|   extends: [ |   extends: ['@antfu', './.eslintrc-auto-import.json', '@unocss'], | ||||||
|     'plugin:vue/vue3-essential', |  | ||||||
|     'eslint:recommended', |  | ||||||
|     'plugin:vue/vue3-recommended', |  | ||||||
|     '@vue/eslint-config-typescript/recommended', |  | ||||||
|     '@vue/eslint-config-prettier', |  | ||||||
|     'plugin:import/recommended', |  | ||||||
|     './.eslintrc-auto-import.json', |  | ||||||
|     '@unocss', |  | ||||||
|   ], |  | ||||||
| 
 | 
 | ||||||
|   settings: { |  | ||||||
|     'import/resolver': { typescript: { project: './tsconfig.app.json' } }, |  | ||||||
|   }, |  | ||||||
|   env: { |  | ||||||
|     'vue/setup-compiler-macros': true, |  | ||||||
|   }, |  | ||||||
|   rules: { |   rules: { | ||||||
|     'vue/multi-word-component-names': ['off'], |     'curly': ['error', 'all'], | ||||||
|     'prettier/prettier': ['error'], |     '@typescript-eslint/semi': ['error', 'always'], | ||||||
|     'import/no-duplicates': ['error', { considerQueryString: true }], |     '@typescript-eslint/no-use-before-define': ['error', { allowNamedExports: true, functions: false }], | ||||||
|     'import/order': ['error', { groups: [['builtin', 'external', 'internal']] }], |     'vue/no-empty-component-block': ['error'], | ||||||
|     'import/extensions': [ |  | ||||||
|       'error', |  | ||||||
|       'ignorePackages', |  | ||||||
|       { |  | ||||||
|         js: 'never', |  | ||||||
|         ts: 'never', |  | ||||||
|         tsx: 'never', |  | ||||||
|       }, |  | ||||||
|     ], |  | ||||||
|     'import/no-unresolved': ['error', { ignore: ['^virtual:'] }], |  | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -31,17 +31,13 @@ declare module '@vue/runtime-core' { | |||||||
|     Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] |     Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] | ||||||
|     CInputText: typeof import('./src/ui/c-input-text/c-input-text.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'] |     'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] | ||||||
|     'CInputText.theme': typeof import('./src/ui/c-input-text/c-input-text.theme.vue')['default'] |  | ||||||
|     CLink: typeof import('./src/ui/c-link/c-link.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'] |     'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] | ||||||
|     CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default'] |     CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default'] | ||||||
|     ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default'] |     ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default'] | ||||||
|     ColoredCard: typeof import('./src/components/ColoredCard.vue')['default'] |     ColoredCard: typeof import('./src/components/ColoredCard.vue')['default'] | ||||||
|     copy: typeof import('./src/ui/c-input-text/c-input-text copy.vue')['default'] |  | ||||||
|     CopyableIpLike: typeof import('./src/tools/ipv4-subnet-calculator/copyable-ip-like.vue')['default'] |  | ||||||
|     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] |     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] | ||||||
|     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] |     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] | ||||||
|     'Demo.routes': typeof import('./src/ui/demo/demo.routes.vue')['default'] |  | ||||||
|     DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] |     DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] | ||||||
|     DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default'] |     DeviceInformation: typeof import('./src/tools/device-information/device-information.vue')['default'] | ||||||
|     DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default'] |     DiffViewer: typeof import('./src/tools/json-diff/diff-viewer/diff-viewer.vue')['default'] | ||||||
| @ -61,22 +57,16 @@ declare module '@vue/runtime-core' { | |||||||
|     HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] |     HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] | ||||||
|     IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] |     IconMdiArrowRightBottom: typeof import('~icons/mdi/arrow-right-bottom')['default'] | ||||||
|     IconMdiCamera: typeof import('~icons/mdi/camera')['default'] |     IconMdiCamera: typeof import('~icons/mdi/camera')['default'] | ||||||
|     IconMdiCameraOutline: typeof import('~icons/mdi/camera-outline')['default'] |  | ||||||
|     IconMdiCameraVideoOff: typeof import('~icons/mdi/camera-video-off')['default'] |  | ||||||
|     IconMdiClose: typeof import('~icons/mdi/close')['default'] |     IconMdiClose: typeof import('~icons/mdi/close')['default'] | ||||||
|     IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] |     IconMdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] | ||||||
|     IconMdiDelete: typeof import('~icons/mdi/delete')['default'] |  | ||||||
|     IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default'] |     IconMdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default'] | ||||||
|     IconMdiDeleteOutlined: typeof import('~icons/mdi/delete-outlined')['default'] |  | ||||||
|     IconMdiDownload: typeof import('~icons/mdi/download')['default'] |     IconMdiDownload: typeof import('~icons/mdi/download')['default'] | ||||||
|     IconMdiEye: typeof import('~icons/mdi/eye')['default'] |     IconMdiEye: typeof import('~icons/mdi/eye')['default'] | ||||||
|     IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] |     IconMdiEyeOff: typeof import('~icons/mdi/eye-off')['default'] | ||||||
|     IconMdiPause: typeof import('~icons/mdi/pause')['default'] |     IconMdiPause: typeof import('~icons/mdi/pause')['default'] | ||||||
|     IconMdiPlay: typeof import('~icons/mdi/play')['default'] |     IconMdiPlay: typeof import('~icons/mdi/play')['default'] | ||||||
|     IconMdiRecord: typeof import('~icons/mdi/record')['default'] |     IconMdiRecord: typeof import('~icons/mdi/record')['default'] | ||||||
|     IconMdiRecordRec: typeof import('~icons/mdi/record-rec')['default'] |  | ||||||
|     IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] |     IconMdiRefresh: typeof import('~icons/mdi/refresh')['default'] | ||||||
|     IconMdiStopCircle: typeof import('~icons/mdi/stop-circle')['default'] |  | ||||||
|     IconMdiVideo: typeof import('~icons/mdi/video')['default'] |     IconMdiVideo: typeof import('~icons/mdi/video')['default'] | ||||||
|     InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] |     InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] | ||||||
|     IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] |     IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] | ||||||
| @ -103,8 +93,6 @@ declare module '@vue/runtime-core' { | |||||||
|     NAlert: typeof import('naive-ui')['NAlert'] |     NAlert: typeof import('naive-ui')['NAlert'] | ||||||
|     NAutoComplete: typeof import('naive-ui')['NAutoComplete'] |     NAutoComplete: typeof import('naive-ui')['NAutoComplete'] | ||||||
|     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] |     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] | ||||||
|     NButton: typeof import('naive-ui')['NButton'] |  | ||||||
|     NCard: typeof import('naive-ui')['NCard'] |  | ||||||
|     NCheckbox: typeof import('naive-ui')['NCheckbox'] |     NCheckbox: typeof import('naive-ui')['NCheckbox'] | ||||||
|     NCode: typeof import('naive-ui')['NCode'] |     NCode: typeof import('naive-ui')['NCode'] | ||||||
|     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] |     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] | ||||||
| @ -116,7 +104,6 @@ declare module '@vue/runtime-core' { | |||||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] |     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||||
|     NForm: typeof import('naive-ui')['NForm'] |     NForm: typeof import('naive-ui')['NForm'] | ||||||
|     NFormItem: typeof import('naive-ui')['NFormItem'] |     NFormItem: typeof import('naive-ui')['NFormItem'] | ||||||
|     NFormItemGi: typeof import('naive-ui')['NFormItemGi'] |  | ||||||
|     NGi: typeof import('naive-ui')['NGi'] |     NGi: typeof import('naive-ui')['NGi'] | ||||||
|     NGrid: typeof import('naive-ui')['NGrid'] |     NGrid: typeof import('naive-ui')['NGrid'] | ||||||
|     NH1: typeof import('naive-ui')['NH1'] |     NH1: typeof import('naive-ui')['NH1'] | ||||||
| @ -137,7 +124,6 @@ declare module '@vue/runtime-core' { | |||||||
|     NScrollbar: typeof import('naive-ui')['NScrollbar'] |     NScrollbar: typeof import('naive-ui')['NScrollbar'] | ||||||
|     NSelect: typeof import('naive-ui')['NSelect'] |     NSelect: typeof import('naive-ui')['NSelect'] | ||||||
|     NSlider: typeof import('naive-ui')['NSlider'] |     NSlider: typeof import('naive-ui')['NSlider'] | ||||||
|     NSpace: typeof import('naive-ui')['NSpace'] |  | ||||||
|     NStatistic: typeof import('naive-ui')['NStatistic'] |     NStatistic: typeof import('naive-ui')['NStatistic'] | ||||||
|     NSwitch: typeof import('naive-ui')['NSwitch'] |     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||||
|     NTable: typeof import('naive-ui')['NTable'] |     NTable: typeof import('naive-ui')['NTable'] | ||||||
|  | |||||||
| @ -79,6 +79,7 @@ | |||||||
|     "yaml": "^2.2.1" |     "yaml": "^2.2.1" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  |     "@antfu/eslint-config": "^0.39.3", | ||||||
|     "@iconify-json/mdi": "^1.1.50", |     "@iconify-json/mdi": "^1.1.50", | ||||||
|     "@playwright/test": "^1.32.3", |     "@playwright/test": "^1.32.3", | ||||||
|     "@rushstack/eslint-patch": "^1.2.0", |     "@rushstack/eslint-patch": "^1.2.0", | ||||||
| @ -100,18 +101,12 @@ | |||||||
|     "@vitejs/plugin-vue": "^2.3.4", |     "@vitejs/plugin-vue": "^2.3.4", | ||||||
|     "@vitejs/plugin-vue-jsx": "^1.3.10", |     "@vitejs/plugin-vue-jsx": "^1.3.10", | ||||||
|     "@vue/compiler-sfc": "^3.2.47", |     "@vue/compiler-sfc": "^3.2.47", | ||||||
|     "@vue/eslint-config-prettier": "^7.1.0", |  | ||||||
|     "@vue/eslint-config-typescript": "^10.0.0", |  | ||||||
|     "@vue/runtime-core": "^3.2.47", |     "@vue/runtime-core": "^3.2.47", | ||||||
|     "@vue/test-utils": "^2.3.2", |     "@vue/test-utils": "^2.3.2", | ||||||
|     "@vue/tsconfig": "^0.1.3", |     "@vue/tsconfig": "^0.1.3", | ||||||
|     "c8": "^7.13.0", |     "c8": "^7.13.0", | ||||||
|     "consola": "^3.0.2", |     "consola": "^3.0.2", | ||||||
|     "eslint": "^8.38.0", |     "eslint": "^8.38.0", | ||||||
|     "eslint-config-prettier": "^8.8.0", |  | ||||||
|     "eslint-import-resolver-typescript": "^3.5.5", |  | ||||||
|     "eslint-plugin-import": "^2.27.5", |  | ||||||
|     "eslint-plugin-vue": "^8.7.1", |  | ||||||
|     "jsdom": "^19.0.0", |     "jsdom": "^19.0.0", | ||||||
|     "less": "^4.1.3", |     "less": "^4.1.3", | ||||||
|     "prettier": "^2.8.7", |     "prettier": "^2.8.7", | ||||||
|  | |||||||
							
								
								
									
										859
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										859
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										16
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								src/App.vue
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| import { useRoute, RouterView } from 'vue-router'; | import { RouterView, useRoute } from 'vue-router'; | ||||||
| import { darkTheme, NGlobalStyle, NMessageProvider, NNotificationProvider } from 'naive-ui'; | import { NGlobalStyle, NMessageProvider, NNotificationProvider, darkTheme } from 'naive-ui'; | ||||||
| import { darkThemeOverrides, lightThemeOverrides } from './themes'; | import { darkThemeOverrides, lightThemeOverrides } from './themes'; | ||||||
| import { layouts } from './layouts'; | import { layouts } from './layouts'; | ||||||
| import { useStyleStore } from './stores/style.store'; | import { useStyleStore } from './stores/style.store'; | ||||||
| @ -16,14 +16,14 @@ const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrid | |||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <n-config-provider :theme="theme" :theme-overrides="themeOverrides"> |   <n-config-provider :theme="theme" :theme-overrides="themeOverrides"> | ||||||
|     <n-global-style /> |     <NGlobalStyle /> | ||||||
|     <n-message-provider placement="bottom"> |     <NMessageProvider placement="bottom"> | ||||||
|       <n-notification-provider placement="bottom-right"> |       <NNotificationProvider placement="bottom-right"> | ||||||
|         <component :is="layout"> |         <component :is="layout"> | ||||||
|           <router-view /> |           <RouterView /> | ||||||
|         </component> |         </component> | ||||||
|       </n-notification-provider> |       </NNotificationProvider> | ||||||
|     </n-message-provider> |     </NMessageProvider> | ||||||
|   </n-config-provider> |   </n-config-provider> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,3 +1,51 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { ChevronRight } from '@vicons/tabler'; | ||||||
|  | import { useStorage } from '@vueuse/core'; | ||||||
|  | import { useThemeVars } from 'naive-ui'; | ||||||
|  | import { computed, h, toRefs } from 'vue'; | ||||||
|  | import { RouterLink, useRoute } from 'vue-router'; | ||||||
|  | import MenuIconItem from './MenuIconItem.vue'; | ||||||
|  | import type { Tool, ToolCategory } from '@/tools/tools.types'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] }); | ||||||
|  | const { toolsByCategory } = toRefs(props); | ||||||
|  | const route = useRoute(); | ||||||
|  | 
 | ||||||
|  | const makeLabel = (tool: Tool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name }); | ||||||
|  | const makeIcon = (tool: Tool) => () => h(MenuIconItem, { tool }); | ||||||
|  | 
 | ||||||
|  | const collapsedCategories = useStorage<Record<string, boolean>>( | ||||||
|  |   'menu-tool-option:collapsed-categories', | ||||||
|  |   {}, | ||||||
|  |   undefined, | ||||||
|  |   { | ||||||
|  |     deep: true, | ||||||
|  |     serializer: { | ||||||
|  |       read: v => (v ? JSON.parse(v) : null), | ||||||
|  |       write: v => JSON.stringify(v), | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | function toggleCategoryCollapse({ name }: { name: string }) { | ||||||
|  |   collapsedCategories.value[name] = !collapsedCategories.value[name]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const menuOptions = computed(() => | ||||||
|  |   toolsByCategory.value.map(({ name, components }) => ({ | ||||||
|  |     name, | ||||||
|  |     isCollapsed: collapsedCategories.value[name], | ||||||
|  |     tools: components.map(tool => ({ | ||||||
|  |       label: makeLabel(tool), | ||||||
|  |       icon: makeIcon(tool), | ||||||
|  |       key: tool.name, | ||||||
|  |     })), | ||||||
|  |   })), | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const themeVars = useThemeVars(); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name"> |   <div v-for="{ name, tools, isCollapsed } of menuOptions" :key="name"> | ||||||
|     <n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })"> |     <n-text tag="div" depth="3" class="category-name" @click="toggleCategoryCollapse({ name })"> | ||||||
| @ -14,7 +62,7 @@ | |||||||
| 
 | 
 | ||||||
|         <n-menu |         <n-menu | ||||||
|           class="menu" |           class="menu" | ||||||
|           :value="(route.name as string)" |           :value="route.name as string" | ||||||
|           :collapsed-width="64" |           :collapsed-width="64" | ||||||
|           :collapsed-icon-size="22" |           :collapsed-icon-size="22" | ||||||
|           :options="tools" |           :options="tools" | ||||||
| @ -26,54 +74,6 @@ | |||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| import type { Tool, ToolCategory } from '@/tools/tools.types'; |  | ||||||
| import { ChevronRight } from '@vicons/tabler'; |  | ||||||
| import { useStorage } from '@vueuse/core'; |  | ||||||
| import { useThemeVars } from 'naive-ui'; |  | ||||||
| import { toRefs, computed, h } from 'vue'; |  | ||||||
| import { RouterLink, useRoute } from 'vue-router'; |  | ||||||
| import MenuIconItem from './MenuIconItem.vue'; |  | ||||||
| 
 |  | ||||||
| const props = withDefaults(defineProps<{ toolsByCategory?: ToolCategory[] }>(), { toolsByCategory: () => [] }); |  | ||||||
| const { toolsByCategory } = toRefs(props); |  | ||||||
| const route = useRoute(); |  | ||||||
| 
 |  | ||||||
| const makeLabel = (tool: Tool) => () => h(RouterLink, { to: tool.path }, { default: () => tool.name }); |  | ||||||
| const makeIcon = (tool: Tool) => () => h(MenuIconItem, { tool }); |  | ||||||
| 
 |  | ||||||
| const collapsedCategories = useStorage<Record<string, boolean>>( |  | ||||||
|   'menu-tool-option:collapsed-categories', |  | ||||||
|   {}, |  | ||||||
|   undefined, |  | ||||||
|   { |  | ||||||
|     deep: true, |  | ||||||
|     serializer: { |  | ||||||
|       read: (v) => (v ? JSON.parse(v) : null), |  | ||||||
|       write: (v) => JSON.stringify(v), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| function toggleCategoryCollapse({ name }: { name: string }) { |  | ||||||
|   collapsedCategories.value[name] = !collapsedCategories.value[name]; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const menuOptions = computed(() => |  | ||||||
|   toolsByCategory.value.map(({ name, components }) => ({ |  | ||||||
|     name: name, |  | ||||||
|     isCollapsed: collapsedCategories.value[name], |  | ||||||
|     tools: components.map((tool) => ({ |  | ||||||
|       label: makeLabel(tool), |  | ||||||
|       icon: makeIcon(tool), |  | ||||||
|       key: tool.name, |  | ||||||
|     })), |  | ||||||
|   })), |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| const themeVars = useThemeVars(); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped lang="less"> | <style scoped lang="less"> | ||||||
| .category-name { | .category-name { | ||||||
|   font-size: 0.93em; |   font-size: 0.93em; | ||||||
|  | |||||||
| @ -1,3 +1,10 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { type Component, toRefs } from 'vue'; | ||||||
|  | 
 | ||||||
|  | const props = defineProps<{ icon: Component; title: string }>(); | ||||||
|  | const { icon, title } = toRefs(props); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <c-card class="colored-card"> |   <c-card class="colored-card"> | ||||||
|     <n-icon class="icon" size="40" :component="icon" /> |     <n-icon class="icon" size="40" :component="icon" /> | ||||||
| @ -13,13 +20,6 @@ | |||||||
|   </c-card> |   </c-card> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| import { toRefs, type Component } from 'vue'; |  | ||||||
| 
 |  | ||||||
| const props = defineProps<{ icon: Component; title: string }>(); |  | ||||||
| const { icon, title } = toRefs(props); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .colored-card { | .colored-card { | ||||||
|   background: rgb(37, 99, 108); |   background: rgb(37, 99, 108); | ||||||
|  | |||||||
| @ -1,29 +1,13 @@ | |||||||
| <template> |  | ||||||
|   <n-tooltip trigger="hover"> |  | ||||||
|     <template #trigger> |  | ||||||
|       <c-button |  | ||||||
|         variant="text" |  | ||||||
|         circle |  | ||||||
|         :type="buttonType" |  | ||||||
|         :style="{ opacity: isFavorite ? 1 : 0.2 }" |  | ||||||
|         @click="toggleFavorite" |  | ||||||
|       > |  | ||||||
|         <n-icon :component="FavoriteFilled" /> |  | ||||||
|       </c-button> |  | ||||||
|     </template> |  | ||||||
|     {{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }} |  | ||||||
|   </n-tooltip> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { FavoriteFilled } from '@vicons/material'; | import { FavoriteFilled } from '@vicons/material'; | ||||||
|  | import { computed, toRefs } from 'vue'; | ||||||
| import { useToolStore } from '@/tools/tools.store'; | import { useToolStore } from '@/tools/tools.store'; | ||||||
| import type { Tool } from '@/tools/tools.types'; | import type { Tool } from '@/tools/tools.types'; | ||||||
| import { computed, toRefs } from 'vue'; | 
 | ||||||
|  | const props = defineProps<{ tool: Tool }>(); | ||||||
| 
 | 
 | ||||||
| const toolStore = useToolStore(); | const toolStore = useToolStore(); | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ tool: Tool }>(); |  | ||||||
| const { tool } = toRefs(props); | const { tool } = toRefs(props); | ||||||
| 
 | 
 | ||||||
| const isFavorite = computed(() => toolStore.isToolFavorite({ tool })); | const isFavorite = computed(() => toolStore.isToolFavorite({ tool })); | ||||||
| @ -40,3 +24,20 @@ function toggleFavorite(event: MouseEvent) { | |||||||
|   toolStore.addToolToFavorites({ tool }); |   toolStore.addToolToFavorites({ tool }); | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <n-tooltip trigger="hover"> | ||||||
|  |     <template #trigger> | ||||||
|  |       <c-button | ||||||
|  |         variant="text" | ||||||
|  |         circle | ||||||
|  |         :type="buttonType" | ||||||
|  |         :style="{ opacity: isFavorite ? 1 : 0.2 }" | ||||||
|  |         @click="toggleFavorite" | ||||||
|  |       > | ||||||
|  |         <n-icon :component="FavoriteFilled" /> | ||||||
|  |       </c-button> | ||||||
|  |     </template> | ||||||
|  |     {{ isFavorite ? 'Remove from favorites' : 'Add to favorites' }} | ||||||
|  |   </n-tooltip> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,5 +1,40 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import type { UseValidationRule } from '@/composable/validation'; | ||||||
|  | import CInputText from '@/ui/c-input-text/c-input-text.vue'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults( | ||||||
|  |   defineProps<{ | ||||||
|  |     transformer?: (v: string) => string | ||||||
|  |     inputValidationRules?: UseValidationRule<string>[] | ||||||
|  |     inputLabel?: string | ||||||
|  |     inputPlaceholder?: string | ||||||
|  |     inputDefault?: string | ||||||
|  |     outputLabel?: string | ||||||
|  |     outputLanguage?: string | ||||||
|  |   }>(), | ||||||
|  |   { | ||||||
|  |     transformer: _.identity, | ||||||
|  |     inputValidationRules: () => [], | ||||||
|  |     inputLabel: 'Input', | ||||||
|  |     inputDefault: '', | ||||||
|  |     inputPlaceholder: 'Input...', | ||||||
|  |     outputLabel: 'Output', | ||||||
|  |     outputLanguage: '', | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } | ||||||
|  |   = toRefs(props); | ||||||
|  | 
 | ||||||
|  | const inputElement = ref<typeof CInputText>(); | ||||||
|  | 
 | ||||||
|  | const input = ref(inputDefault.value); | ||||||
|  | const output = computed(() => transformer.value(input.value)); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <c-input-text |   <CInputText | ||||||
|     ref="inputElement" |     ref="inputElement" | ||||||
|     v-model:value="input" |     v-model:value="input" | ||||||
|     :placeholder="inputPlaceholder" |     :placeholder="inputPlaceholder" | ||||||
| @ -13,44 +48,9 @@ | |||||||
|   /> |   /> | ||||||
| 
 | 
 | ||||||
|   <div> |   <div> | ||||||
|     <div mb-5px>{{ outputLabel }}</div> |     <div mb-5px> | ||||||
|  |       {{ outputLabel }} | ||||||
|  |     </div> | ||||||
|     <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" /> |     <textarea-copyable :value="output" :language="outputLanguage" :follow-height-of="inputElement?.inputWrapperRef" /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import type { UseValidationRule } from '@/composable/validation'; |  | ||||||
| import _ from 'lodash'; |  | ||||||
| import CInputText from '@/ui/c-input-text/c-input-text.vue'; |  | ||||||
| 
 |  | ||||||
| const props = withDefaults( |  | ||||||
|   defineProps<{ |  | ||||||
|     transformer?: (v: string) => string; |  | ||||||
|     inputValidationRules?: UseValidationRule<string>[]; |  | ||||||
|     inputLabel?: string; |  | ||||||
|     inputPlaceholder?: string; |  | ||||||
|     inputDefault?: string; |  | ||||||
|     outputLabel?: string; |  | ||||||
|     outputLanguage?: string; |  | ||||||
|   }>(), |  | ||||||
|   { |  | ||||||
|     transformer: _.identity, |  | ||||||
|     inputValidationRules: () => [], |  | ||||||
|     inputLabel: 'Input', |  | ||||||
|     inputDefault: '', |  | ||||||
|     inputPlaceholder: 'Input...', |  | ||||||
|     outputLabel: 'Output', |  | ||||||
|     outputLanguage: '', |  | ||||||
|   }, |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| const { transformer, inputValidationRules, inputLabel, outputLabel, outputLanguage, inputPlaceholder, inputDefault } = |  | ||||||
|   toRefs(props); |  | ||||||
| 
 |  | ||||||
| const inputElement = ref<typeof CInputText>(); |  | ||||||
| 
 |  | ||||||
| const input = ref(inputDefault.value); |  | ||||||
| const output = computed(() => transformer.value(input.value)); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped></style> |  | ||||||
|  | |||||||
| @ -1,20 +1,5 @@ | |||||||
| <template> |  | ||||||
|   <c-input-text v-model:value="value"> |  | ||||||
|     <template #suffix> |  | ||||||
|       <n-tooltip trigger="hover"> |  | ||||||
|         <template #trigger> |  | ||||||
|           <c-button circle variant="text" size="small" @click="onCopyClicked"> |  | ||||||
|             <icon-mdi-content-copy /> |  | ||||||
|           </c-button> |  | ||||||
|         </template> |  | ||||||
|         {{ tooltipText }} |  | ||||||
|       </n-tooltip> |  | ||||||
|     </template> |  | ||||||
|   </c-input-text> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useVModel, useClipboard } from '@vueuse/core'; | import { useClipboard, useVModel } from '@vueuse/core'; | ||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ value: string }>(); | const props = defineProps<{ value: string }>(); | ||||||
| @ -34,3 +19,18 @@ function onCopyClicked() { | |||||||
|   }, 2000); |   }, 2000); | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <c-input-text v-model:value="value"> | ||||||
|  |     <template #suffix> | ||||||
|  |       <n-tooltip trigger="hover"> | ||||||
|  |         <template #trigger> | ||||||
|  |           <c-button circle variant="text" size="small" @click="onCopyClicked"> | ||||||
|  |             <icon-mdi-content-copy /> | ||||||
|  |           </c-button> | ||||||
|  |         </template> | ||||||
|  |         {{ tooltipText }} | ||||||
|  |       </n-tooltip> | ||||||
|  |     </template> | ||||||
|  |   </c-input-text> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,14 +1,7 @@ | |||||||
| <template> |  | ||||||
|   <div class="menu-icon-item"> |  | ||||||
|     <n-icon :component="tool.icon" /> |  | ||||||
|     <div v-if="tool.isNew" class="badge"></div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { Tool } from '@/tools/tools.types'; |  | ||||||
| import { useThemeVars } from 'naive-ui'; | import { useThemeVars } from 'naive-ui'; | ||||||
| import { toRefs } from 'vue'; | import { toRefs } from 'vue'; | ||||||
|  | import type { Tool } from '@/tools/tools.types'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ tool: Tool }>(); | const props = defineProps<{ tool: Tool }>(); | ||||||
| const { tool } = toRefs(props); | const { tool } = toRefs(props); | ||||||
| @ -16,6 +9,13 @@ const { tool } = toRefs(props); | |||||||
| const theme = useThemeVars(); | const theme = useThemeVars(); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | <template> | ||||||
|  |   <div class="menu-icon-item"> | ||||||
|  |     <n-icon :component="tool.icon" /> | ||||||
|  |     <div v-if="tool.isNew" class="badge" /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .menu-icon-item { | .menu-icon-item { | ||||||
|   position: relative; |   position: relative; | ||||||
|  | |||||||
| @ -1,3 +1,12 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, toRefs } from 'vue'; | ||||||
|  | import { useStyleStore } from '@/stores/style.store'; | ||||||
|  | 
 | ||||||
|  | const styleStore = useStyleStore(); | ||||||
|  | const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore); | ||||||
|  | const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static')); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <n-layout has-sider> |   <n-layout has-sider> | ||||||
|     <n-layout-sider |     <n-layout-sider | ||||||
| @ -19,15 +28,6 @@ | |||||||
|   </n-layout> |   </n-layout> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useStyleStore } from '@/stores/style.store'; |  | ||||||
| import { toRefs, computed } from 'vue'; |  | ||||||
| 
 |  | ||||||
| const styleStore = useStyleStore(); |  | ||||||
| const { isMenuCollapsed, isSmallScreen } = toRefs(styleStore); |  | ||||||
| const siderPosition = computed(() => (isSmallScreen.value ? 'absolute' : 'static')); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .overlay { | .overlay { | ||||||
|   position: absolute; |   position: absolute; | ||||||
|  | |||||||
| @ -1,3 +1,21 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler'; | ||||||
|  | import { toRefs } from 'vue'; | ||||||
|  | import { useStyleStore } from '@/stores/style.store'; | ||||||
|  | import { useThemeStore } from '@/ui/theme/theme.store'; | ||||||
|  | 
 | ||||||
|  | const styleStore = useStyleStore(); | ||||||
|  | const { isDarkTheme } = toRefs(styleStore); | ||||||
|  | 
 | ||||||
|  | const themeStore = useThemeStore(); | ||||||
|  | 
 | ||||||
|  | function toggleDarkTheme() { | ||||||
|  |   isDarkTheme.value = !isDarkTheme.value; | ||||||
|  | 
 | ||||||
|  |   themeStore.toggleTheme(); | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <n-tooltip trigger="hover"> |   <n-tooltip trigger="hover"> | ||||||
|     <template #trigger> |     <template #trigger> | ||||||
| @ -51,24 +69,6 @@ | |||||||
|   </n-tooltip> |   </n-tooltip> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useStyleStore } from '@/stores/style.store'; |  | ||||||
| import { useThemeStore } from '@/ui/theme/theme.store'; |  | ||||||
| import { BrandGithub, BrandTwitter, InfoCircle, Moon, Sun } from '@vicons/tabler'; |  | ||||||
| import { toRefs } from 'vue'; |  | ||||||
| 
 |  | ||||||
| const styleStore = useStyleStore(); |  | ||||||
| const { isDarkTheme } = toRefs(styleStore); |  | ||||||
| 
 |  | ||||||
| const themeStore = useThemeStore(); |  | ||||||
| 
 |  | ||||||
| function toggleDarkTheme() { |  | ||||||
|   isDarkTheme.value = !isDarkTheme.value; |  | ||||||
| 
 |  | ||||||
|   themeStore.toggleTheme(); |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .n-button { | .n-button { | ||||||
|   &:not(:last-child) { |   &:not(:last-child) { | ||||||
|  | |||||||
| @ -1,14 +1,14 @@ | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { useFuzzySearch } from '@/composable/fuzzySearch'; |  | ||||||
| import { useTracker } from '@/modules/tracker/tracker.services'; |  | ||||||
| import { tools } from '@/tools'; |  | ||||||
| import type { Tool } from '@/tools/tools.types'; |  | ||||||
| import { SearchRound } from '@vicons/material'; | import { SearchRound } from '@vicons/material'; | ||||||
| import { useMagicKeys, whenever } from '@vueuse/core'; | import { useMagicKeys, whenever } from '@vueuse/core'; | ||||||
| import type { NInput } from 'naive-ui'; | import { NInput } from 'naive-ui'; | ||||||
| import { computed, h, ref } from 'vue'; | import { computed, h, ref } from 'vue'; | ||||||
| import { useRouter } from 'vue-router'; | import { useRouter } from 'vue-router'; | ||||||
| import SearchBarItem from './SearchBarItem.vue'; | import SearchBarItem from './SearchBarItem.vue'; | ||||||
|  | import type { Tool } from '@/tools/tools.types'; | ||||||
|  | import { tools } from '@/tools'; | ||||||
|  | import { useTracker } from '@/modules/tracker/tracker.services'; | ||||||
|  | import { useFuzzySearch } from '@/composable/fuzzySearch'; | ||||||
| 
 | 
 | ||||||
| const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool }); | const toolToOption = (tool: Tool) => ({ label: tool.name, value: tool.path, tool }); | ||||||
| 
 | 
 | ||||||
| @ -20,6 +20,12 @@ const inputEl = ref<HTMLElement>(); | |||||||
| const displayDropDown = ref(true); | const displayDropDown = ref(true); | ||||||
| const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac')); | const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac')); | ||||||
| 
 | 
 | ||||||
|  | const { searchResult } = useFuzzySearch({ | ||||||
|  |   search: queryString, | ||||||
|  |   data: tools, | ||||||
|  |   options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
| const options = computed(() => { | const options = computed(() => { | ||||||
|   if (queryString.value === '') { |   if (queryString.value === '') { | ||||||
|     return tools.map(toolToOption); |     return tools.map(toolToOption); | ||||||
| @ -28,12 +34,6 @@ const options = computed(() => { | |||||||
|   return searchResult.value.map(toolToOption); |   return searchResult.value.map(toolToOption); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const { searchResult } = useFuzzySearch({ |  | ||||||
|   search: queryString, |  | ||||||
|   data: tools, |  | ||||||
|   options: { keys: [{ name: 'name', weight: 2 }, 'description', 'keywords'] }, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const keys = useMagicKeys({ | const keys = useMagicKeys({ | ||||||
|   passive: false, |   passive: false, | ||||||
|   onEventFired(e) { |   onEventFired(e) { | ||||||
| @ -83,13 +83,13 @@ function onFocus() { | |||||||
|       :options="options" |       :options="options" | ||||||
|       :on-select="(value: string | number) => onSelect(String(value))" |       :on-select="(value: string | number) => onSelect(String(value))" | ||||||
|       :render-label="renderOption" |       :render-label="renderOption" | ||||||
|       :default-value="'aa'" |       default-value="aa" | ||||||
|       :get-show="() => displayDropDown" |       :get-show="() => displayDropDown" | ||||||
|       :on-focus="onFocus" |       :on-focus="onFocus" | ||||||
|       @update:value="() => (displayDropDown = true)" |       @update:value="() => (displayDropDown = true)" | ||||||
|     > |     > | ||||||
|       <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }"> |       <template #default="{ handleInput, handleBlur, handleFocus, value: slotValue }"> | ||||||
|         <n-input |         <NInput | ||||||
|           ref="inputEl" |           ref="inputEl" | ||||||
|           round |           round | ||||||
|           clearable |           clearable | ||||||
| @ -103,7 +103,7 @@ function onFocus() { | |||||||
|           <template #prefix> |           <template #prefix> | ||||||
|             <n-icon :component="SearchRound" /> |             <n-icon :component="SearchRound" /> | ||||||
|           </template> |           </template> | ||||||
|         </n-input> |         </NInput> | ||||||
|       </template> |       </template> | ||||||
|     </n-auto-complete> |     </n-auto-complete> | ||||||
|   </div> |   </div> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import type { Tool } from '@/tools/tools.types'; |  | ||||||
| import { toRefs } from 'vue'; | import { toRefs } from 'vue'; | ||||||
|  | import type { Tool } from '@/tools/tools.types'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ tool: Tool }>(); | const props = defineProps<{ tool: Tool }>(); | ||||||
| const { tool } = toRefs(props); | const { tool } = toRefs(props); | ||||||
| @ -11,8 +11,12 @@ const { tool } = toRefs(props); | |||||||
|     <n-icon class="icon" :component="tool.icon" /> |     <n-icon class="icon" :component="tool.icon" /> | ||||||
| 
 | 
 | ||||||
|     <div> |     <div> | ||||||
|       <div class="name">{{ tool.name }}</div> |       <div class="name"> | ||||||
|       <div class="description">{{ tool.description }}</div> |         {{ tool.name }} | ||||||
|  |       </div> | ||||||
|  |       <div class="description"> | ||||||
|  |         {{ tool.description }} | ||||||
|  |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  | |||||||
| @ -1,12 +1,3 @@ | |||||||
| <template> |  | ||||||
|   <n-tooltip trigger="hover"> |  | ||||||
|     <template #trigger> |  | ||||||
|       <span class="value" @click="handleClick">{{ value }}</span> |  | ||||||
|     </template> |  | ||||||
|     {{ tooltipText }} |  | ||||||
|   </n-tooltip> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useClipboard } from '@vueuse/core'; | import { useClipboard } from '@vueuse/core'; | ||||||
| import { ref, toRefs } from 'vue'; | import { ref, toRefs } from 'vue'; | ||||||
| @ -27,6 +18,15 @@ function handleClick() { | |||||||
| } | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | <template> | ||||||
|  |   <n-tooltip trigger="hover"> | ||||||
|  |     <template #trigger> | ||||||
|  |       <span class="value" @click="handleClick">{{ value }}</span> | ||||||
|  |     </template> | ||||||
|  |     {{ tooltipText }} | ||||||
|  |   </n-tooltip> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
| <style scoped lang="less"> | <style scoped lang="less"> | ||||||
| .value { | .value { | ||||||
|   cursor: pointer; |   cursor: pointer; | ||||||
|  | |||||||
| @ -1,3 +1,49 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { Copy } from '@vicons/tabler'; | ||||||
|  | import { useClipboard, useElementSize } from '@vueuse/core'; | ||||||
|  | import hljs from 'highlight.js/lib/core'; | ||||||
|  | import jsonHljs from 'highlight.js/lib/languages/json'; | ||||||
|  | import sqlHljs from 'highlight.js/lib/languages/sql'; | ||||||
|  | import xmlHljs from 'highlight.js/lib/languages/xml'; | ||||||
|  | import yamlHljs from 'highlight.js/lib/languages/yaml'; | ||||||
|  | import { ref, toRefs } from 'vue'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults( | ||||||
|  |   defineProps<{ | ||||||
|  |     value: string | ||||||
|  |     followHeightOf?: HTMLElement | null | ||||||
|  |     language?: string | ||||||
|  |     copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none' | ||||||
|  |     copyMessage?: string | ||||||
|  |   }>(), | ||||||
|  |   { | ||||||
|  |     followHeightOf: null, | ||||||
|  |     language: 'txt', | ||||||
|  |     copyPlacement: 'top-right', | ||||||
|  |     copyMessage: 'Copy to clipboard', | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | hljs.registerLanguage('sql', sqlHljs); | ||||||
|  | hljs.registerLanguage('json', jsonHljs); | ||||||
|  | hljs.registerLanguage('html', xmlHljs); | ||||||
|  | hljs.registerLanguage('yaml', yamlHljs); | ||||||
|  | 
 | ||||||
|  | const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); | ||||||
|  | const { height } = followHeightOf.value ? useElementSize(followHeightOf) : { height: ref(null) }; | ||||||
|  | 
 | ||||||
|  | const { copy } = useClipboard({ source: value }); | ||||||
|  | const tooltipText = ref(copyMessage.value); | ||||||
|  | 
 | ||||||
|  | function onCopyClicked() { | ||||||
|  |   copy(); | ||||||
|  |   tooltipText.value = 'Copied !'; | ||||||
|  | 
 | ||||||
|  |   setTimeout(() => { | ||||||
|  |     tooltipText.value = copyMessage.value; | ||||||
|  |   }, 2000); | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div style="overflow-x: hidden; width: 100%"> |   <div style="overflow-x: hidden; width: 100%"> | ||||||
|     <c-card class="result-card"> |     <c-card class="result-card"> | ||||||
| @ -22,57 +68,13 @@ | |||||||
|       </n-tooltip> |       </n-tooltip> | ||||||
|     </c-card> |     </c-card> | ||||||
|     <div v-if="copyPlacement === 'outside'" mt-4 flex justify-center> |     <div v-if="copyPlacement === 'outside'" mt-4 flex justify-center> | ||||||
|       <c-button @click="onCopyClicked"> {{ tooltipText }} </c-button> |       <c-button @click="onCopyClicked"> | ||||||
|  |         {{ tooltipText }} | ||||||
|  |       </c-button> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| import { Copy } from '@vicons/tabler'; |  | ||||||
| import { useClipboard, useElementSize } from '@vueuse/core'; |  | ||||||
| import hljs from 'highlight.js/lib/core'; |  | ||||||
| import jsonHljs from 'highlight.js/lib/languages/json'; |  | ||||||
| import sqlHljs from 'highlight.js/lib/languages/sql'; |  | ||||||
| import xmlHljs from 'highlight.js/lib/languages/xml'; |  | ||||||
| import yamlHljs from 'highlight.js/lib/languages/yaml'; |  | ||||||
| import { ref, toRefs } from 'vue'; |  | ||||||
| 
 |  | ||||||
| hljs.registerLanguage('sql', sqlHljs); |  | ||||||
| hljs.registerLanguage('json', jsonHljs); |  | ||||||
| hljs.registerLanguage('html', xmlHljs); |  | ||||||
| hljs.registerLanguage('yaml', yamlHljs); |  | ||||||
| 
 |  | ||||||
| const props = withDefaults( |  | ||||||
|   defineProps<{ |  | ||||||
|     value: string; |  | ||||||
|     followHeightOf?: HTMLElement | null; |  | ||||||
|     language?: string; |  | ||||||
|     copyPlacement?: 'top-right' | 'bottom-right' | 'outside' | 'none'; |  | ||||||
|     copyMessage?: string; |  | ||||||
|   }>(), |  | ||||||
|   { |  | ||||||
|     followHeightOf: null, |  | ||||||
|     language: 'txt', |  | ||||||
|     copyPlacement: 'top-right', |  | ||||||
|     copyMessage: 'Copy to clipboard', |  | ||||||
|   }, |  | ||||||
| ); |  | ||||||
| const { value, language, followHeightOf, copyPlacement, copyMessage } = toRefs(props); |  | ||||||
| const { height } = followHeightOf ? useElementSize(followHeightOf) : { height: ref(null) }; |  | ||||||
| 
 |  | ||||||
| const { copy } = useClipboard({ source: value }); |  | ||||||
| const tooltipText = ref(copyMessage.value); |  | ||||||
| 
 |  | ||||||
| function onCopyClicked() { |  | ||||||
|   copy(); |  | ||||||
|   tooltipText.value = 'Copied !'; |  | ||||||
| 
 |  | ||||||
|   setTimeout(() => { |  | ||||||
|     tooltipText.value = copyMessage.value; |  | ||||||
|   }, 2000); |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| ::v-deep(.n-scrollbar) { | ::v-deep(.n-scrollbar) { | ||||||
|   padding-bottom: 10px; |   padding-bottom: 10px; | ||||||
|  | |||||||
| @ -1,3 +1,17 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useThemeVars } from 'naive-ui'; | ||||||
|  | import { toRefs } from 'vue'; | ||||||
|  | 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> | <template> | ||||||
|   <router-link :to="tool.path"> |   <router-link :to="tool.path"> | ||||||
|     <c-card class="tool-card"> |     <c-card class="tool-card"> | ||||||
| @ -16,7 +30,7 @@ | |||||||
|             New |             New | ||||||
|           </n-tag> |           </n-tag> | ||||||
| 
 | 
 | ||||||
|           <favorite-button :tool="tool" /> |           <FavoriteButton :tool="tool" /> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <n-h3 class="title"> |       <n-h3 class="title"> | ||||||
| @ -26,27 +40,13 @@ | |||||||
|       <div class="description"> |       <div class="description"> | ||||||
|         <n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px"> |         <n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px"> | ||||||
|           {{ tool.description }} |           {{ tool.description }} | ||||||
|           <br />  |           <br>  | ||||||
|         </n-ellipsis> |         </n-ellipsis> | ||||||
|       </div> |       </div> | ||||||
|     </c-card> |     </c-card> | ||||||
|   </router-link> |   </router-link> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| import type { Tool } from '@/tools/tools.types'; |  | ||||||
| import { useThemeVars } from 'naive-ui'; |  | ||||||
| import { toRefs } from 'vue'; |  | ||||||
| import { useAppTheme } from '@/ui/theme/themes'; |  | ||||||
| import FavoriteButton from './FavoriteButton.vue'; |  | ||||||
| 
 |  | ||||||
| const props = defineProps<{ tool: Tool & { category: string } }>(); |  | ||||||
| const { tool } = toRefs(props); |  | ||||||
| const theme = useThemeVars(); |  | ||||||
| 
 |  | ||||||
| const appTheme = useAppTheme(); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| a { | a { | ||||||
|   text-decoration: none; |   text-decoration: none; | ||||||
|  | |||||||
| @ -11,7 +11,8 @@ function computedRefreshable<T>(getter: () => T, { throttle }: { throttle?: numb | |||||||
| 
 | 
 | ||||||
|   if (throttle) { |   if (throttle) { | ||||||
|     watchThrottled(getter, update, { throttle }); |     watchThrottled(getter, update, { throttle }); | ||||||
|   } else { |   } | ||||||
|  |   else { | ||||||
|     watch(getter, update); |     watch(getter, update); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { useClipboard, type MaybeRef, get } from '@vueuse/core'; | import { type MaybeRef, get, useClipboard } from '@vueuse/core'; | ||||||
| import { useMessage } from 'naive-ui'; | import { useMessage } from 'naive-ui'; | ||||||
| 
 | 
 | ||||||
| export function useCopy({ source, text = 'Copied to the clipboard' }: { source: MaybeRef<unknown>; text?: string }) { | export function useCopy({ source, text = 'Copied to the clipboard' }: { source: MaybeRef<unknown>; text?: string }) { | ||||||
|  | |||||||
| @ -5,8 +5,8 @@ function getFileExtensionFromBase64({ | |||||||
|   base64String, |   base64String, | ||||||
|   defaultExtension = 'txt', |   defaultExtension = 'txt', | ||||||
| }: { | }: { | ||||||
|   base64String: string; |   base64String: string | ||||||
|   defaultExtension?: string; |   defaultExtension?: string | ||||||
| }) { | }) { | ||||||
|   const hasMimeType = base64String.match(/data:(.*?);base64/i); |   const hasMimeType = base64String.match(/data:(.*?);base64/i); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { get, type MaybeRef } from '@vueuse/core'; | import { type MaybeRef, get } from '@vueuse/core'; | ||||||
| import Fuse from 'fuse.js'; | import Fuse from 'fuse.js'; | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| 
 | 
 | ||||||
| @ -9,9 +9,9 @@ function useFuzzySearch<Data>({ | |||||||
|   data, |   data, | ||||||
|   options = {}, |   options = {}, | ||||||
| }: { | }: { | ||||||
|   search: MaybeRef<string>; |   search: MaybeRef<string> | ||||||
|   data: Data[]; |   data: Data[] | ||||||
|   options?: Fuse.IFuseOptions<Data>; |   options?: Fuse.IFuseOptions<Data> | ||||||
| }) { | }) { | ||||||
|   const fuse = new Fuse(data, options); |   const fuse = new Fuse(data, options); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,4 +1,3 @@ | |||||||
| /* eslint-disable @typescript-eslint/no-empty-function */ |  | ||||||
| import { describe, expect, it } from 'vitest'; | import { describe, expect, it } from 'vitest'; | ||||||
| import { isFalsyOrHasThrown } from './validation'; | import { isFalsyOrHasThrown } from './validation'; | ||||||
| 
 | 
 | ||||||
| @ -11,7 +10,7 @@ describe('useValidation', () => { | |||||||
|       expect(isFalsyOrHasThrown(() => {})).toBe(true); |       expect(isFalsyOrHasThrown(() => {})).toBe(true); | ||||||
|       expect( |       expect( | ||||||
|         isFalsyOrHasThrown(() => { |         isFalsyOrHasThrown(() => { | ||||||
|           throw new Error(); |           throw new Error('message'); | ||||||
|         }), |         }), | ||||||
|       ).toBe(true); |       ).toBe(true); | ||||||
|     }); |     }); | ||||||
|  | |||||||
| @ -1,45 +1,48 @@ | |||||||
| import { get, type MaybeRef } from '@vueuse/core'; | import { type MaybeRef, get } from '@vueuse/core'; | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| import { reactive, watch, type Ref } from 'vue'; | import { type Ref, reactive, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
| type ValidatorReturnType = unknown; | type ValidatorReturnType = unknown; | ||||||
| 
 | 
 | ||||||
| export interface UseValidationRule<T> { | export interface UseValidationRule<T> { | ||||||
|   validator: (value: T) => ValidatorReturnType; |   validator: (value: T) => ValidatorReturnType | ||||||
|   message: string; |   message: string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { | export function isFalsyOrHasThrown(cb: () => ValidatorReturnType): boolean { | ||||||
|   try { |   try { | ||||||
|     const returnValue = cb(); |     const returnValue = cb(); | ||||||
| 
 | 
 | ||||||
|     if (_.isNil(returnValue)) return true; |     if (_.isNil(returnValue)) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     return returnValue === false; |     return returnValue === false; | ||||||
|   } catch (_) { |   } | ||||||
|  |   catch (_) { | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export type ValidationAttrs = { | export interface ValidationAttrs { | ||||||
|   feedback: string; |   feedback: string | ||||||
|   validationStatus: string | undefined; |   validationStatus: string | undefined | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export function useValidation<T>({ | export function useValidation<T>({ | ||||||
|   source, |   source, | ||||||
|   rules, |   rules, | ||||||
|   watch: watchRefs = [], |   watch: watchRefs = [], | ||||||
| }: { | }: { | ||||||
|   source: Ref<T>; |   source: Ref<T> | ||||||
|   rules: MaybeRef<UseValidationRule<T>[]>; |   rules: MaybeRef<UseValidationRule<T>[]> | ||||||
|   watch?: Ref<unknown>[]; |   watch?: Ref<unknown>[] | ||||||
| }) { | }) { | ||||||
|   const state = reactive<{ |   const state = reactive<{ | ||||||
|     message: string; |     message: string | ||||||
|     status: undefined | 'error'; |     status: undefined | 'error' | ||||||
|     isValid: boolean; |     isValid: boolean | ||||||
|     attrs: ValidationAttrs; |     attrs: ValidationAttrs | ||||||
|   }>({ |   }>({ | ||||||
|     message: '', |     message: '', | ||||||
|     status: undefined, |     status: undefined, | ||||||
|  | |||||||
| @ -2,7 +2,11 @@ | |||||||
| import { NIcon, useThemeVars } from 'naive-ui'; | import { NIcon, useThemeVars } from 'naive-ui'; | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| import { RouterLink } from 'vue-router'; | import { RouterLink } from 'vue-router'; | ||||||
| import { Heart, Menu2, Home2 } from '@vicons/tabler'; | import { Heart, Home2, Menu2 } from '@vicons/tabler'; | ||||||
|  | import SearchBar from '../components/SearchBar.vue'; | ||||||
|  | import HeroGradient from '../assets/hero-gradient.svg?component'; | ||||||
|  | import MenuLayout from '../components/MenuLayout.vue'; | ||||||
|  | import NavbarButtons from '../components/NavbarButtons.vue'; | ||||||
| import { toolsByCategory } from '@/tools'; | import { toolsByCategory } from '@/tools'; | ||||||
| import { useStyleStore } from '@/stores/style.store'; | import { useStyleStore } from '@/stores/style.store'; | ||||||
| import { config } from '@/config'; | import { config } from '@/config'; | ||||||
| @ -10,10 +14,6 @@ import type { ToolCategory } from '@/tools/tools.types'; | |||||||
| import { useToolStore } from '@/tools/tools.store'; | import { useToolStore } from '@/tools/tools.store'; | ||||||
| import { useTracker } from '@/modules/tracker/tracker.services'; | import { useTracker } from '@/modules/tracker/tracker.services'; | ||||||
| import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue'; | import CollapsibleToolMenu from '@/components/CollapsibleToolMenu.vue'; | ||||||
| import SearchBar from '../components/SearchBar.vue'; |  | ||||||
| import HeroGradient from '../assets/hero-gradient.svg?component'; |  | ||||||
| import MenuLayout from '../components/MenuLayout.vue'; |  | ||||||
| import NavbarButtons from '../components/NavbarButtons.vue'; |  | ||||||
| 
 | 
 | ||||||
| const themeVars = useThemeVars(); | const themeVars = useThemeVars(); | ||||||
| const styleStore = useStyleStore(); | const styleStore = useStyleStore(); | ||||||
| @ -31,23 +31,27 @@ const tools = computed<ToolCategory[]>(() => [ | |||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <menu-layout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }"> |   <MenuLayout class="menu-layout" :class="{ isSmallScreen: styleStore.isSmallScreen }"> | ||||||
|     <template #sider> |     <template #sider> | ||||||
|       <router-link to="/" class="hero-wrapper"> |       <RouterLink to="/" class="hero-wrapper"> | ||||||
|         <hero-gradient class="gradient" /> |         <HeroGradient class="gradient" /> | ||||||
|         <div class="text-wrapper"> |         <div class="text-wrapper"> | ||||||
|           <div class="title">IT - TOOLS</div> |           <div class="title"> | ||||||
|  |             IT - TOOLS | ||||||
|  |           </div> | ||||||
|           <div class="divider" /> |           <div class="divider" /> | ||||||
|           <div class="subtitle">Handy tools for developers</div> |           <div class="subtitle"> | ||||||
|  |             Handy tools for developers | ||||||
|  |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </router-link> |       </RouterLink> | ||||||
| 
 | 
 | ||||||
|       <div class="sider-content"> |       <div class="sider-content"> | ||||||
|         <div v-if="styleStore.isSmallScreen" flex justify-center> |         <div v-if="styleStore.isSmallScreen" flex justify-center> | ||||||
|           <navbar-buttons /> |           <NavbarButtons /> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <collapsible-tool-menu :tools-by-category="tools" /> |         <CollapsibleToolMenu :tools-by-category="tools" /> | ||||||
| 
 | 
 | ||||||
|         <div class="footer"> |         <div class="footer"> | ||||||
|           <div> |           <div> | ||||||
| @ -71,7 +75,9 @@ const tools = computed<ToolCategory[]>(() => [ | |||||||
|           </div> |           </div> | ||||||
|           <div> |           <div> | ||||||
|             © {{ new Date().getFullYear() }} |             © {{ new Date().getFullYear() }} | ||||||
|             <c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh"> Corentin Thomasset </c-link> |             <c-link target="_blank" rel="noopener" href="https://github.com/CorentinTh"> | ||||||
|  |               Corentin Thomasset | ||||||
|  |             </c-link> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| @ -86,21 +92,21 @@ const tools = computed<ToolCategory[]>(() => [ | |||||||
|           aria-label="Toggle menu" |           aria-label="Toggle menu" | ||||||
|           @click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed" |           @click="styleStore.isMenuCollapsed = !styleStore.isMenuCollapsed" | ||||||
|         > |         > | ||||||
|           <n-icon size="25" :component="Menu2" /> |           <NIcon size="25" :component="Menu2" /> | ||||||
|         </c-button> |         </c-button> | ||||||
| 
 | 
 | ||||||
|         <n-tooltip trigger="hover"> |         <n-tooltip trigger="hover"> | ||||||
|           <template #trigger> |           <template #trigger> | ||||||
|             <c-button to="/" circle variant="text" aria-label="Home"> |             <c-button to="/" circle variant="text" aria-label="Home"> | ||||||
|               <n-icon size="25" :component="Home2" /> |               <NIcon size="25" :component="Home2" /> | ||||||
|             </c-button> |             </c-button> | ||||||
|           </template> |           </template> | ||||||
|           Home |           Home | ||||||
|         </n-tooltip> |         </n-tooltip> | ||||||
| 
 | 
 | ||||||
|         <search-bar /> |         <SearchBar /> | ||||||
| 
 | 
 | ||||||
|         <navbar-buttons v-if="!styleStore.isSmallScreen" /> |         <NavbarButtons v-if="!styleStore.isSmallScreen" /> | ||||||
| 
 | 
 | ||||||
|         <n-tooltip trigger="hover"> |         <n-tooltip trigger="hover"> | ||||||
|           <template #trigger> |           <template #trigger> | ||||||
| @ -114,7 +120,7 @@ const tools = computed<ToolCategory[]>(() => [ | |||||||
|               @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" |               @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" | ||||||
|             > |             > | ||||||
|               Buy me a coffee |               Buy me a coffee | ||||||
|               <n-icon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 /> |               <NIcon v-if="!styleStore.isSmallScreen" :component="Heart" ml-2 /> | ||||||
|             </c-button> |             </c-button> | ||||||
|           </template> |           </template> | ||||||
|           ❤ Support IT Tools development ! |           ❤ Support IT Tools development ! | ||||||
| @ -122,7 +128,7 @@ const tools = computed<ToolCategory[]>(() => [ | |||||||
|       </div> |       </div> | ||||||
|       <slot /> |       <slot /> | ||||||
|     </template> |     </template> | ||||||
|   </menu-layout> |   </MenuLayout> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
|  | |||||||
| @ -3,9 +3,9 @@ import { useRoute } from 'vue-router'; | |||||||
| import { useHead } from '@vueuse/head'; | import { useHead } from '@vueuse/head'; | ||||||
| import type { HeadObject } from '@vueuse/head'; | import type { HeadObject } from '@vueuse/head'; | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
|  | import BaseLayout from './base.layout.vue'; | ||||||
| import FavoriteButton from '@/components/FavoriteButton.vue'; | import FavoriteButton from '@/components/FavoriteButton.vue'; | ||||||
| import type { Tool } from '@/tools/tools.types'; | import type { Tool } from '@/tools/tools.types'; | ||||||
| import BaseLayout from './base.layout.vue'; |  | ||||||
| 
 | 
 | ||||||
| const route = useRoute(); | const route = useRoute(); | ||||||
| 
 | 
 | ||||||
| @ -26,7 +26,7 @@ useHead(head); | |||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <base-layout> |   <BaseLayout> | ||||||
|     <div class="tool-layout"> |     <div class="tool-layout"> | ||||||
|       <div class="tool-header"> |       <div class="tool-header"> | ||||||
|         <div flex flex-nowrap items-center justify-between> |         <div flex flex-nowrap items-center justify-between> | ||||||
| @ -35,7 +35,7 @@ useHead(head); | |||||||
|           </n-h1> |           </n-h1> | ||||||
| 
 | 
 | ||||||
|           <div> |           <div> | ||||||
|             <favorite-button :tool="{name: route.meta.name} as Tool" /> |             <FavoriteButton :tool="{ name: route.meta.name } as Tool" /> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
| @ -50,7 +50,7 @@ useHead(head); | |||||||
|     <div class="tool-content"> |     <div class="tool-content"> | ||||||
|       <slot /> |       <slot /> | ||||||
|     </div> |     </div> | ||||||
|   </base-layout> |   </BaseLayout> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
|  | |||||||
| @ -1,19 +1,19 @@ | |||||||
| import { createApp } from 'vue'; | import { createApp } from 'vue'; | ||||||
| import { createPinia } from 'pinia'; | import { createPinia } from 'pinia'; | ||||||
| import { createHead } from '@vueuse/head'; | import { createHead } from '@vueuse/head'; | ||||||
| // eslint-disable-next-line import/no-unresolved
 | 
 | ||||||
| import { registerSW } from 'virtual:pwa-register'; | import { registerSW } from 'virtual:pwa-register'; | ||||||
| import { plausible } from './plugins/plausible.plugin'; | import { plausible } from './plugins/plausible.plugin'; | ||||||
| 
 | 
 | ||||||
| import 'virtual:uno.css'; | import 'virtual:uno.css'; | ||||||
| 
 | 
 | ||||||
| registerSW(); |  | ||||||
| 
 |  | ||||||
| import { naive } from './plugins/naive.plugin'; | import { naive } from './plugins/naive.plugin'; | ||||||
| 
 | 
 | ||||||
| import App from './App.vue'; | import App from './App.vue'; | ||||||
| import router from './router'; | import router from './router'; | ||||||
| 
 | 
 | ||||||
|  | registerSW(); | ||||||
|  | 
 | ||||||
| const app = createApp(App); | const app = createApp(App); | ||||||
| 
 | 
 | ||||||
| app.use(createPinia()); | app.use(createPinia()); | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ function useTracker() { | |||||||
|   const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible'); |   const plausible: ReturnType<typeof Plausible> | undefined = inject('plausible'); | ||||||
| 
 | 
 | ||||||
|   if (_.isNil(plausible)) { |   if (_.isNil(plausible)) { | ||||||
|     throw new Error('Plausible must be instantiated'); |     throw new TypeError('Plausible must be instantiated'); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const tracker = createTrackerService({ plausible }); |   const tracker = createTrackerService({ plausible }); | ||||||
|  | |||||||
| @ -9,10 +9,18 @@ useHead({ title: 'Page not found - IT Tools' }); | |||||||
|   <div mt-20 flex flex-col items-center> |   <div mt-20 flex flex-col items-center> | ||||||
|     <n-icon :component="Coffee" size="100" depth="3" /> |     <n-icon :component="Coffee" size="100" depth="3" /> | ||||||
| 
 | 
 | ||||||
|     <n-h1 m-0 mt-3>404 Not Found</n-h1> |     <n-h1 m-0 mt-3> | ||||||
|     <n-text mt-4 block depth="3">Sorry, this page does not seem to exist</n-text> |       404 Not Found | ||||||
|     <n-text mb-8 block depth="3">Maybe the cache is doing tricky things, try force-refreshing?</n-text> |     </n-h1> | ||||||
|  |     <n-text mt-4 block depth="3"> | ||||||
|  |       Sorry, this page does not seem to exist | ||||||
|  |     </n-text> | ||||||
|  |     <n-text mb-8 block depth="3"> | ||||||
|  |       Maybe the cache is doing tricky things, try force-refreshing? | ||||||
|  |     </n-text> | ||||||
| 
 | 
 | ||||||
|     <c-button to="/"> Back home </c-button> |     <c-button to="/"> | ||||||
|  |       Back home | ||||||
|  |     </c-button> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useTracker } from '@/modules/tracker/tracker.services'; |  | ||||||
| import { useHead } from '@vueuse/head'; | import { useHead } from '@vueuse/head'; | ||||||
|  | import { useTracker } from '@/modules/tracker/tracker.services'; | ||||||
| 
 | 
 | ||||||
| useHead({ title: 'About - IT Tools' }); | useHead({ title: 'About - IT Tools' }); | ||||||
| const { tracker } = useTracker(); | const { tracker } = useTracker(); | ||||||
| @ -11,7 +11,9 @@ const { tracker } = useTracker(); | |||||||
|     <n-h1>About</n-h1> |     <n-h1>About</n-h1> | ||||||
|     <n-p> |     <n-p> | ||||||
|       This wonderful website, made with ❤ by |       This wonderful website, made with ❤ by | ||||||
|       <c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener"> Corentin Thomasset </c-link>, |       <c-link href="https://github.com/CorentinTh" target="_blank" rel="noopener"> | ||||||
|  |         Corentin Thomasset | ||||||
|  |       </c-link>, | ||||||
|       aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to share |       aggregates useful tools for developer and people working in IT. If you find it useful, please fell free to share | ||||||
|       it to people you think may find it useful too and don't forget to pin it in your shortcut bar ! |       it to people you think may find it useful too and don't forget to pin it in your shortcut bar ! | ||||||
|     </n-p> |     </n-p> | ||||||
| @ -25,8 +27,8 @@ const { tracker } = useTracker(); | |||||||
|         target="_blank" |         target="_blank" | ||||||
|         @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" |         @click="() => tracker.trackEvent({ eventName: 'Support button clicked' })" | ||||||
|       > |       > | ||||||
|         sponsoring me </c-link |         sponsoring me | ||||||
|       >. |       </c-link>. | ||||||
|     </n-p> |     </n-p> | ||||||
| 
 | 
 | ||||||
|     <n-h2>Technologies</n-h2> |     <n-h2>Technologies</n-h2> | ||||||
|  | |||||||
| @ -1,10 +1,10 @@ | |||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { config } from '@/config'; |  | ||||||
| import { useToolStore } from '@/tools/tools.store'; |  | ||||||
| import { Heart } from '@vicons/tabler'; | import { Heart } from '@vicons/tabler'; | ||||||
| import { useHead } from '@vueuse/head'; | import { useHead } from '@vueuse/head'; | ||||||
| import ColoredCard from '../components/ColoredCard.vue'; | import ColoredCard from '../components/ColoredCard.vue'; | ||||||
| import ToolCard from '../components/ToolCard.vue'; | import ToolCard from '../components/ToolCard.vue'; | ||||||
|  | import { useToolStore } from '@/tools/tools.store'; | ||||||
|  | import { config } from '@/config'; | ||||||
| 
 | 
 | ||||||
| const toolStore = useToolStore(); | const toolStore = useToolStore(); | ||||||
| 
 | 
 | ||||||
| @ -16,25 +16,23 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); | |||||||
|     <div class="grid-wrapper"> |     <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-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> | ||||||
|         <n-gi> |         <n-gi> | ||||||
|           <colored-card title="You like it-tools?" :icon="Heart"> |           <ColoredCard title="You like it-tools?" :icon="Heart"> | ||||||
|             Give us a star on |             Give us a star on | ||||||
|             <a |             <a | ||||||
|               href="https://github.com/CorentinTh/it-tools" |               href="https://github.com/CorentinTh/it-tools" | ||||||
|               rel="noopener" |               rel="noopener" | ||||||
|               target="_blank" |               target="_blank" | ||||||
|               aria-label="IT-Tools' GitHub repository" |               aria-label="IT-Tools' GitHub repository" | ||||||
|               >GitHub</a |             >GitHub</a> | ||||||
|             > |  | ||||||
|             or follow us on |             or follow us on | ||||||
|             <a |             <a | ||||||
|               href="https://twitter.com/ittoolsdottech" |               href="https://twitter.com/ittoolsdottech" | ||||||
|               rel="noopener" |               rel="noopener" | ||||||
|               target="_blank" |               target="_blank" | ||||||
|               aria-label="IT-Tools' Twitter account" |               aria-label="IT-Tools' Twitter account" | ||||||
|               >Twitter</a |             >Twitter</a>! Thank you | ||||||
|             >! Thank you |  | ||||||
|             <n-icon :component="Heart" /> |             <n-icon :component="Heart" /> | ||||||
|           </colored-card> |           </ColoredCard> | ||||||
|         </n-gi> |         </n-gi> | ||||||
|       </n-grid> |       </n-grid> | ||||||
| 
 | 
 | ||||||
| @ -43,7 +41,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); | |||||||
|           <n-h3>Your favorite tools</n-h3> |           <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-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"> |             <n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name"> | ||||||
|               <tool-card :tool="tool" /> |               <ToolCard :tool="tool" /> | ||||||
|             </n-gi> |             </n-gi> | ||||||
|           </n-grid> |           </n-grid> | ||||||
|         </div> |         </div> | ||||||
| @ -53,7 +51,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); | |||||||
|         <n-h3>Newest tools</n-h3> |         <n-h3>Newest tools</n-h3> | ||||||
|         <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> |         <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"> |           <n-gi v-for="tool in toolStore.newTools" :key="tool.name"> | ||||||
|             <tool-card :tool="tool" /> |             <ToolCard :tool="tool" /> | ||||||
|           </n-gi> |           </n-gi> | ||||||
|         </n-grid> |         </n-grid> | ||||||
|       </div> |       </div> | ||||||
| @ -62,7 +60,7 @@ useHead({ title: 'IT Tools - Handy online tools for developers' }); | |||||||
|       <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> |       <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"> |         <n-gi v-for="tool in toolStore.tools" :key="tool.name"> | ||||||
|           <transition> |           <transition> | ||||||
|             <tool-card :tool="tool" /> |             <ToolCard :tool="tool" /> | ||||||
|           </transition> |           </transition> | ||||||
|         </n-gi> |         </n-gi> | ||||||
|       </n-grid> |       </n-grid> | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
| import { config } from '@/config'; |  | ||||||
| import { noop } from 'lodash'; | import { noop } from 'lodash'; | ||||||
| 
 | 
 | ||||||
| import Plausible from 'plausible-tracker'; | import Plausible from 'plausible-tracker'; | ||||||
| import type { App } from 'vue'; | import type { App } from 'vue'; | ||||||
|  | import { config } from '@/config'; | ||||||
| 
 | 
 | ||||||
| function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> { | function createFakePlausibleInstance(): Pick<ReturnType<typeof Plausible>, 'trackEvent' | 'enableAutoPageviews'> { | ||||||
|   return { |   return { | ||||||
| @ -15,11 +15,11 @@ function createPlausibleInstance({ | |||||||
|   config, |   config, | ||||||
| }: { | }: { | ||||||
|   config: { |   config: { | ||||||
|     isTrackerEnabled: boolean; |     isTrackerEnabled: boolean | ||||||
|     domain: string; |     domain: string | ||||||
|     apiHost: string; |     apiHost: string | ||||||
|     trackLocalhost: boolean; |     trackLocalhost: boolean | ||||||
|   }; |   } | ||||||
| }) { | }) { | ||||||
|   if (config.isTrackerEnabled) { |   if (config.isTrackerEnabled) { | ||||||
|     return Plausible(config); |     return Plausible(config); | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ const toolsRoutes = tools.map(({ path, name, component, ...config }) => ({ | |||||||
| const toolsRedirectRoutes = tools | const toolsRedirectRoutes = tools | ||||||
|   .filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0) |   .filter(({ redirectFrom }) => redirectFrom && redirectFrom.length > 0) | ||||||
|   .flatMap( |   .flatMap( | ||||||
|     ({ path, redirectFrom }) => redirectFrom?.map((redirectSource) => ({ path: redirectSource, redirect: path })) ?? [], |     ({ path, redirectFrom }) => redirectFrom?.map(redirectSource => ({ path: redirectSource, redirect: path })) ?? [], | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
| const router = createRouter({ | const router = createRouter({ | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { useMediaQuery, useStorage } from '@vueuse/core'; | import { useMediaQuery, useStorage } from '@vueuse/core'; | ||||||
| import { defineStore } from 'pinia'; | import { defineStore } from 'pinia'; | ||||||
| import { watch, type Ref } from 'vue'; | import { type Ref, watch } from 'vue'; | ||||||
| 
 | 
 | ||||||
| export const useStyleStore = defineStore('style', { | export const useStyleStore = defineStore('style', { | ||||||
|   state: () => { |   state: () => { | ||||||
| @ -8,7 +8,7 @@ export const useStyleStore = defineStore('style', { | |||||||
|     const isSmallScreen = useMediaQuery('(max-width: 700px)'); |     const isSmallScreen = useMediaQuery('(max-width: 700px)'); | ||||||
|     const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>; |     const isMenuCollapsed = useStorage('isMenuCollapsed', isSmallScreen.value) as Ref<boolean>; | ||||||
| 
 | 
 | ||||||
|     watch(isSmallScreen, (v) => (isMenuCollapsed.value = v)); |     watch(isSmallScreen, v => (isMenuCollapsed.value = v)); | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|       isDarkTheme, |       isDarkTheme, | ||||||
|  | |||||||
| @ -1,3 +1,51 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { Upload } from '@vicons/tabler'; | ||||||
|  | import { useBase64 } from '@vueuse/core'; | ||||||
|  | import type { UploadFileInfo } from 'naive-ui'; | ||||||
|  | import { type Ref, ref } from 'vue'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  | import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||||||
|  | import { useValidation } from '@/composable/validation'; | ||||||
|  | import { isValidBase64 } from '@/utils/base64'; | ||||||
|  | 
 | ||||||
|  | const base64Input = ref(''); | ||||||
|  | const { download } = useDownloadFileFromBase64({ source: base64Input }); | ||||||
|  | const base64InputValidation = useValidation({ | ||||||
|  |   source: base64Input, | ||||||
|  |   rules: [ | ||||||
|  |     { | ||||||
|  |       message: 'Invalid base 64 string', | ||||||
|  |       validator: value => isValidBase64(value.trim()), | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function downloadFile() { | ||||||
|  |   if (!base64InputValidation.isValid) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   try { | ||||||
|  |     download(); | ||||||
|  |   } | ||||||
|  |   catch (_) { | ||||||
|  |     // | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const fileList = ref(); | ||||||
|  | const fileInput = ref() as Ref<File>; | ||||||
|  | const { base64: fileBase64 } = useBase64(fileInput); | ||||||
|  | const { copy: copyFileBase64 } = useCopy({ source: fileBase64, text: 'Base64 string copied to the clipboard' }); | ||||||
|  | 
 | ||||||
|  | async function onUpload({ file: { file } }: { file: UploadFileInfo }) { | ||||||
|  |   if (file) { | ||||||
|  |     fileList.value = []; | ||||||
|  |     fileInput.value = file; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <c-card title="Base64 to file"> |   <c-card title="Base64 to file"> | ||||||
|     <c-input-text |     <c-input-text | ||||||
| @ -22,63 +70,22 @@ | |||||||
|         <div mb-2> |         <div mb-2> | ||||||
|           <n-icon size="35" :depth="3" :component="Upload" /> |           <n-icon size="35" :depth="3" :component="Upload" /> | ||||||
|         </div> |         </div> | ||||||
|         <n-text style="font-size: 14px"> Click or drag a file to this area to upload </n-text> |         <n-text style="font-size: 14px"> | ||||||
|  |           Click or drag a file to this area to upload | ||||||
|  |         </n-text> | ||||||
|       </n-upload-dragger> |       </n-upload-dragger> | ||||||
|     </n-upload> |     </n-upload> | ||||||
| 
 | 
 | ||||||
|     <c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 /> |     <c-input-text :value="fileBase64" multiline readonly placeholder="File in base64 will be here" rows="5" mb-2 /> | ||||||
| 
 | 
 | ||||||
|     <div flex justify-center> |     <div flex justify-center> | ||||||
|       <c-button @click="copyFileBase64()"> Copy </c-button> |       <c-button @click="copyFileBase64()"> | ||||||
|  |         Copy | ||||||
|  |       </c-button> | ||||||
|     </div> |     </div> | ||||||
|   </c-card> |   </c-card> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useCopy } from '@/composable/copy'; |  | ||||||
| import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; |  | ||||||
| import { useValidation } from '@/composable/validation'; |  | ||||||
| import { isValidBase64 } from '@/utils/base64'; |  | ||||||
| import { Upload } from '@vicons/tabler'; |  | ||||||
| import { useBase64 } from '@vueuse/core'; |  | ||||||
| import type { UploadFileInfo } from 'naive-ui'; |  | ||||||
| import { ref, type Ref } from 'vue'; |  | ||||||
| 
 |  | ||||||
| const base64Input = ref(''); |  | ||||||
| const { download } = useDownloadFileFromBase64({ source: base64Input }); |  | ||||||
| const base64InputValidation = useValidation({ |  | ||||||
|   source: base64Input, |  | ||||||
|   rules: [ |  | ||||||
|     { |  | ||||||
|       message: 'Invalid base 64 string', |  | ||||||
|       validator: (value) => isValidBase64(value.trim()), |  | ||||||
|     }, |  | ||||||
|   ], |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| function downloadFile() { |  | ||||||
|   if (!base64InputValidation.isValid) return; |  | ||||||
| 
 |  | ||||||
|   try { |  | ||||||
|     download(); |  | ||||||
|   } catch (_) { |  | ||||||
|     // |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const fileList = ref(); |  | ||||||
| const fileInput = ref() as Ref<File>; |  | ||||||
| const { base64: fileBase64 } = useBase64(fileInput); |  | ||||||
| const { copy: copyFileBase64 } = useCopy({ source: fileBase64, text: 'Base64 string copied to the clipboard' }); |  | ||||||
| 
 |  | ||||||
| async function onUpload({ file: { file } }: { file: UploadFileInfo }) { |  | ||||||
|   if (file) { |  | ||||||
|     fileList.value = []; |  | ||||||
|     fileInput.value = file; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| ::v-deep(.n-upload-trigger) { | ::v-deep(.n-upload-trigger) { | ||||||
|   width: 100%; |   width: 100%; | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ import { defineTool } from '../tool'; | |||||||
| export const tool = defineTool({ | export const tool = defineTool({ | ||||||
|   name: 'Base64 file converter', |   name: 'Base64 file converter', | ||||||
|   path: '/base64-file-converter', |   path: '/base64-file-converter', | ||||||
|   description: "Convert string, files or images into a it's base64 representation.", |   description: 'Convert string, files or images into a it\'s base64 representation.', | ||||||
|   keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'], |   keywords: ['base64', 'converter', 'upload', 'image', 'file', 'conversion', 'web', 'data', 'format'], | ||||||
|   component: () => import('./base64-file-converter.vue'), |   component: () => import('./base64-file-converter.vue'), | ||||||
|   icon: FileDigit, |   icon: FileDigit, | ||||||
|  | |||||||
| @ -1,3 +1,30 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  | import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; | ||||||
|  | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
|  | 
 | ||||||
|  | const encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false); | ||||||
|  | const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false); | ||||||
|  | 
 | ||||||
|  | const textInput = ref(''); | ||||||
|  | const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.value })); | ||||||
|  | const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' }); | ||||||
|  | 
 | ||||||
|  | const base64Input = ref(''); | ||||||
|  | const textOutput = computed(() => | ||||||
|  |   withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''), | ||||||
|  | ); | ||||||
|  | const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); | ||||||
|  | const b64ValidationRules = [ | ||||||
|  |   { | ||||||
|  |     message: 'Invalid base64 string', | ||||||
|  |     validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }), | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | const b64ValidationWatch = [decodeUrlSafe]; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <c-card title="String to base64"> |   <c-card title="String to base64"> | ||||||
|     <n-form-item label="Encode URL safe" label-placement="left"> |     <n-form-item label="Encode URL safe" label-placement="left"> | ||||||
| @ -24,7 +51,9 @@ | |||||||
|     /> |     /> | ||||||
| 
 | 
 | ||||||
|     <div flex justify-center> |     <div flex justify-center> | ||||||
|       <c-button @click="copyTextBase64()"> Copy base64 </c-button> |       <c-button @click="copyTextBase64()"> | ||||||
|  |         Copy base64 | ||||||
|  |       </c-button> | ||||||
|     </div> |     </div> | ||||||
|   </c-card> |   </c-card> | ||||||
| 
 | 
 | ||||||
| @ -54,34 +83,9 @@ | |||||||
|     /> |     /> | ||||||
| 
 | 
 | ||||||
|     <div flex justify-center> |     <div flex justify-center> | ||||||
|       <c-button @click="copyText()"> Copy decoded string </c-button> |       <c-button @click="copyText()"> | ||||||
|  |         Copy decoded string | ||||||
|  |       </c-button> | ||||||
|     </div> |     </div> | ||||||
|   </c-card> |   </c-card> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useCopy } from '@/composable/copy'; |  | ||||||
| import { base64ToText, isValidBase64, textToBase64 } from '@/utils/base64'; |  | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; |  | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| 
 |  | ||||||
| const encodeUrlSafe = useStorage('base64-string-converter--encode-url-safe', false); |  | ||||||
| const decodeUrlSafe = useStorage('base64-string-converter--decode-url-safe', false); |  | ||||||
| 
 |  | ||||||
| const textInput = ref(''); |  | ||||||
| const base64Output = computed(() => textToBase64(textInput.value, { makeUrlSafe: encodeUrlSafe.value })); |  | ||||||
| const { copy: copyTextBase64 } = useCopy({ source: base64Output, text: 'Base64 string copied to the clipboard' }); |  | ||||||
| 
 |  | ||||||
| const base64Input = ref(''); |  | ||||||
| const textOutput = computed(() => |  | ||||||
|   withDefaultOnError(() => base64ToText(base64Input.value.trim(), { makeUrlSafe: decodeUrlSafe.value }), ''), |  | ||||||
| ); |  | ||||||
| const { copy: copyText } = useCopy({ source: textOutput, text: 'String copied to the clipboard' }); |  | ||||||
| const b64ValidationRules = [ |  | ||||||
|   { |  | ||||||
|     message: 'Invalid base64 string', |  | ||||||
|     validator: (value: string) => isValidBase64(value.trim(), { makeUrlSafe: decodeUrlSafe.value }), |  | ||||||
|   }, |  | ||||||
| ]; |  | ||||||
| const b64ValidationWatch = [decodeUrlSafe]; |  | ||||||
| </script> |  | ||||||
|  | |||||||
| @ -1,3 +1,15 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  | import { textToBase64 } from '@/utils/base64'; | ||||||
|  | 
 | ||||||
|  | const username = ref(''); | ||||||
|  | const password = ref(''); | ||||||
|  | const header = computed(() => `Authorization: Basic ${textToBase64(`${username.value}:${password.value}`)}`); | ||||||
|  | 
 | ||||||
|  | const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 /> |     <c-input-text v-model:value="username" label="Username" placeholder="Your username..." clearable raw-text mb-5 /> | ||||||
| @ -19,23 +31,13 @@ | |||||||
|       </n-statistic> |       </n-statistic> | ||||||
|     </c-card> |     </c-card> | ||||||
|     <div mt-5 flex justify-center> |     <div mt-5 flex justify-center> | ||||||
|       <c-button @click="copy">Copy header</c-button> |       <c-button @click="copy"> | ||||||
|  |         Copy header | ||||||
|  |       </c-button> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useCopy } from '@/composable/copy'; |  | ||||||
| import { textToBase64 } from '@/utils/base64'; |  | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| 
 |  | ||||||
| const username = ref(''); |  | ||||||
| const password = ref(''); |  | ||||||
| const header = computed(() => `Authorization: Basic ${textToBase64(`${username.value}:${password.value}`)}`); |  | ||||||
| 
 |  | ||||||
| const { copy } = useCopy({ source: header, text: 'Header copied to the clipboard' }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| ::v-deep(.n-statistic-value__content) { | ::v-deep(.n-statistic-value__content) { | ||||||
|   font-family: monospace; |   font-family: monospace; | ||||||
|  | |||||||
| @ -1,3 +1,21 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import { compareSync, hashSync } from 'bcryptjs'; | ||||||
|  | import { useThemeVars } from 'naive-ui'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  | 
 | ||||||
|  | const themeVars = useThemeVars(); | ||||||
|  | 
 | ||||||
|  | const input = ref(''); | ||||||
|  | const saltCount = ref(10); | ||||||
|  | const hashed = computed(() => hashSync(input.value, saltCount.value)); | ||||||
|  | const { copy } = useCopy({ source: hashed, text: 'Hashed string copied to the clipboard' }); | ||||||
|  | 
 | ||||||
|  | const compareString = ref(''); | ||||||
|  | const compareHash = ref(''); | ||||||
|  | const compareMatch = computed(() => compareSync(compareString.value, compareHash.value)); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <c-card title="Hash"> |   <c-card title="Hash"> | ||||||
|     <c-input-text |     <c-input-text | ||||||
| @ -16,7 +34,9 @@ | |||||||
|     <c-input-text :value="hashed" readonly text-center /> |     <c-input-text :value="hashed" readonly text-center /> | ||||||
| 
 | 
 | ||||||
|     <div mt-5 flex justify-center> |     <div mt-5 flex justify-center> | ||||||
|       <c-button @click="copy"> Copy hash </c-button> |       <c-button @click="copy"> | ||||||
|  |         Copy hash | ||||||
|  |       </c-button> | ||||||
|     </div> |     </div> | ||||||
|   </c-card> |   </c-card> | ||||||
| 
 | 
 | ||||||
| @ -37,24 +57,6 @@ | |||||||
|   </c-card> |   </c-card> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| import { hashSync, compareSync } from 'bcryptjs'; |  | ||||||
| import { useCopy } from '@/composable/copy'; |  | ||||||
| import { useThemeVars } from 'naive-ui'; |  | ||||||
| 
 |  | ||||||
| const themeVars = useThemeVars(); |  | ||||||
| 
 |  | ||||||
| const input = ref(''); |  | ||||||
| const saltCount = ref(10); |  | ||||||
| const hashed = computed(() => hashSync(input.value, saltCount.value)); |  | ||||||
| const { copy } = useCopy({ source: hashed, text: 'Hashed string copied to the clipboard' }); |  | ||||||
| 
 |  | ||||||
| const compareString = ref(''); |  | ||||||
| const compareHash = ref(''); |  | ||||||
| const compareMatch = computed(() => compareSync(compareString.value, compareHash.value)); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .compare-result { | .compare-result { | ||||||
|   color: v-bind('themeVars.errorColor'); |   color: v-bind('themeVars.errorColor'); | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ function computeAverage({ data }: { data: number[] }) { | |||||||
| function computeVariance({ data }: { data: number[] }) { | function computeVariance({ data }: { data: number[] }) { | ||||||
|   const mean = computeAverage({ data }); |   const mean = computeAverage({ data }); | ||||||
| 
 | 
 | ||||||
|   const squaredDiffs = data.map((value) => Math.pow(value - mean, 2)); |   const squaredDiffs = data.map(value => (value - mean) ** 2); | ||||||
| 
 | 
 | ||||||
|   return computeAverage({ data: squaredDiffs }); |   return computeAverage({ data: squaredDiffs }); | ||||||
| } | } | ||||||
| @ -24,11 +24,11 @@ function arrayToMarkdownTable({ data, headerMap = {} }: { data: unknown[]; heade | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const headers = Object.keys(data[0]); |   const headers = Object.keys(data[0]); | ||||||
|   const rows = data.map((obj) => Object.values(obj)); |   const rows = data.map(obj => Object.values(obj)); | ||||||
| 
 | 
 | ||||||
|   const headerRow = `| ${headers.map((header) => headerMap[header] ?? header).join(' | ')} |`; |   const headerRow = `| ${headers.map(header => headerMap[header] ?? header).join(' | ')} |`; | ||||||
|   const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`; |   const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`; | ||||||
|   const dataRows = rows.map((row) => `| ${row.join(' | ')} |`).join('\n'); |   const dataRows = rows.map(row => `| ${row.join(' | ')} |`).join('\n'); | ||||||
| 
 | 
 | ||||||
|   return `${headerRow}\n${separatorRow}\n${dataRows}`; |   return `${headerRow}\n${separatorRow}\n${dataRows}`; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,89 +1,9 @@ | |||||||
| <template> |  | ||||||
|   <n-scrollbar style="flex: 1" x-scrollable> |  | ||||||
|     <div mb-5 flex flex-1 flex-nowrap justify-center gap-12px> |  | ||||||
|       <div v-for="(suite, index) of suites" :key="index"> |  | ||||||
|         <c-card style="width: 294px"> |  | ||||||
|           <c-input-text |  | ||||||
|             v-model:value="suite.title" |  | ||||||
|             label-position="left" |  | ||||||
|             label="Suite name" |  | ||||||
|             placeholder="Suite name..." |  | ||||||
|             clearable |  | ||||||
|           /> |  | ||||||
| 
 |  | ||||||
|           <n-divider></n-divider> |  | ||||||
|           <n-form-item label="Suite values" :show-feedback="false"> |  | ||||||
|             <dynamic-values v-model:values="suite.data" /> |  | ||||||
|           </n-form-item> |  | ||||||
|         </c-card> |  | ||||||
| 
 |  | ||||||
|         <div flex justify-center> |  | ||||||
|           <c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)"> |  | ||||||
|             <n-icon :component="Trash" depth="3" mr-2 size="18" /> |  | ||||||
|             Delete suite |  | ||||||
|           </c-button> |  | ||||||
|           <c-button |  | ||||||
|             variant="text" |  | ||||||
|             @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })" |  | ||||||
|           > |  | ||||||
|             <n-icon :component="Plus" depth="3" mr-2 size="18" /> |  | ||||||
|             Add suite |  | ||||||
|           </c-button> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </n-scrollbar> |  | ||||||
| 
 |  | ||||||
|   <div style="flex: 0 0 100%"> |  | ||||||
|     <div style="max-width: 600px; margin: 0 auto"> |  | ||||||
|       <div mx-auto max-w-sm flex justify-center gap-3> |  | ||||||
|         <c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 /> |  | ||||||
| 
 |  | ||||||
|         <c-button |  | ||||||
|           @click=" |  | ||||||
|             suites = [ |  | ||||||
|               { title: 'Suite 1', data: [] }, |  | ||||||
|               { title: 'Suite 2', data: [] }, |  | ||||||
|             ] |  | ||||||
|           " |  | ||||||
|           >Reset suites</c-button |  | ||||||
|         > |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <n-table> |  | ||||||
|         <thead> |  | ||||||
|           <tr> |  | ||||||
|             <th>{{ header.position }}</th> |  | ||||||
|             <th>{{ header.title }}</th> |  | ||||||
|             <th>{{ header.size }}</th> |  | ||||||
|             <th>{{ header.mean }}</th> |  | ||||||
|             <th>{{ header.variance }}</th> |  | ||||||
|           </tr> |  | ||||||
|         </thead> |  | ||||||
|         <tbody> |  | ||||||
|           <tr v-for="{ title, size, mean, variance, position } of results" :key="title"> |  | ||||||
|             <td>{{ position }}</td> |  | ||||||
|             <td>{{ title }}</td> |  | ||||||
|             <td>{{ size }}</td> |  | ||||||
|             <td>{{ mean }}</td> |  | ||||||
|             <td>{{ variance }}</td> |  | ||||||
|           </tr> |  | ||||||
|         </tbody> |  | ||||||
|       </n-table> |  | ||||||
|       <div mt-5 flex justify-center gap-3> |  | ||||||
|         <c-button @click="copyAsMarkdown">Copy as markdown table</c-button> |  | ||||||
|         <c-button @click="copyAsBulletList">Copy as bullet list</c-button> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { Trash, Plus } from '@vicons/tabler'; | import { Plus, Trash } from '@vicons/tabler'; | ||||||
| import { useClipboard, useStorage } from '@vueuse/core'; | import { useClipboard, useStorage } from '@vueuse/core'; | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| import { computeAverage, computeVariance, arrayToMarkdownTable } from './benchmark-builder.models'; | import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchmark-builder.models'; | ||||||
| import DynamicValues from './dynamic-values.vue'; | import DynamicValues from './dynamic-values.vue'; | ||||||
| 
 | 
 | ||||||
| const suites = useStorage('benchmark-builder:suites', [ | const suites = useStorage('benchmark-builder:suites', [ | ||||||
| @ -114,8 +34,8 @@ const results = computed(() => { | |||||||
|       const deltaWithBestMean = mean - bestMean; |       const deltaWithBestMean = mean - bestMean; | ||||||
|       const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean); |       const ratioWithBestMean = bestMean === 0 ? '∞' : round(mean / bestMean); | ||||||
| 
 | 
 | ||||||
|       const comparisonValues: string = |       const comparisonValues: string | ||||||
|         index !== 0 && bestMean !== mean ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : ''; |         = (index !== 0 && bestMean !== mean) ? ` (+${round(deltaWithBestMean)}${cleanUnit} ; x${ratioWithBestMean})` : ''; | ||||||
| 
 | 
 | ||||||
|       return { |       return { | ||||||
|         position: index + 1, |         position: index + 1, | ||||||
| @ -157,4 +77,87 @@ function copyAsBulletList() { | |||||||
| } | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="less" scoped></style> | <template> | ||||||
|  |   <n-scrollbar style="flex: 1" x-scrollable> | ||||||
|  |     <div mb-5 flex flex-1 flex-nowrap justify-center gap-12px> | ||||||
|  |       <div v-for="(suite, index) of suites" :key="index"> | ||||||
|  |         <c-card style="width: 294px"> | ||||||
|  |           <c-input-text | ||||||
|  |             v-model:value="suite.title" | ||||||
|  |             label-position="left" | ||||||
|  |             label="Suite name" | ||||||
|  |             placeholder="Suite name..." | ||||||
|  |             clearable | ||||||
|  |           /> | ||||||
|  | 
 | ||||||
|  |           <n-divider /> | ||||||
|  |           <n-form-item label="Suite values" :show-feedback="false"> | ||||||
|  |             <DynamicValues v-model:values="suite.data" /> | ||||||
|  |           </n-form-item> | ||||||
|  |         </c-card> | ||||||
|  | 
 | ||||||
|  |         <div flex justify-center> | ||||||
|  |           <c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)"> | ||||||
|  |             <n-icon :component="Trash" depth="3" mr-2 size="18" /> | ||||||
|  |             Delete suite | ||||||
|  |           </c-button> | ||||||
|  |           <c-button | ||||||
|  |             variant="text" | ||||||
|  |             @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })" | ||||||
|  |           > | ||||||
|  |             <n-icon :component="Plus" depth="3" mr-2 size="18" /> | ||||||
|  |             Add suite | ||||||
|  |           </c-button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </n-scrollbar> | ||||||
|  | 
 | ||||||
|  |   <div style="flex: 0 0 100%"> | ||||||
|  |     <div style="max-width: 600px; margin: 0 auto"> | ||||||
|  |       <div mx-auto max-w-sm flex justify-center gap-3> | ||||||
|  |         <c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 /> | ||||||
|  | 
 | ||||||
|  |         <c-button | ||||||
|  |           @click=" | ||||||
|  |             suites = [ | ||||||
|  |               { title: 'Suite 1', data: [] }, | ||||||
|  |               { title: 'Suite 2', data: [] }, | ||||||
|  |             ] | ||||||
|  |           " | ||||||
|  |         > | ||||||
|  |           Reset suites | ||||||
|  |         </c-button> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <n-table> | ||||||
|  |         <thead> | ||||||
|  |           <tr> | ||||||
|  |             <th>{{ header.position }}</th> | ||||||
|  |             <th>{{ header.title }}</th> | ||||||
|  |             <th>{{ header.size }}</th> | ||||||
|  |             <th>{{ header.mean }}</th> | ||||||
|  |             <th>{{ header.variance }}</th> | ||||||
|  |           </tr> | ||||||
|  |         </thead> | ||||||
|  |         <tbody> | ||||||
|  |           <tr v-for="{ title, size, mean, variance, position } of results" :key="title"> | ||||||
|  |             <td>{{ position }}</td> | ||||||
|  |             <td>{{ title }}</td> | ||||||
|  |             <td>{{ size }}</td> | ||||||
|  |             <td>{{ mean }}</td> | ||||||
|  |             <td>{{ variance }}</td> | ||||||
|  |           </tr> | ||||||
|  |         </tbody> | ||||||
|  |       </n-table> | ||||||
|  |       <div mt-5 flex justify-center gap-3> | ||||||
|  |         <c-button @click="copyAsMarkdown"> | ||||||
|  |           Copy as markdown table | ||||||
|  |         </c-button> | ||||||
|  |         <c-button @click="copyAsBulletList"> | ||||||
|  |           Copy as bullet list | ||||||
|  |         </c-button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,7 +1,37 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { Plus, Trash } from '@vicons/tabler'; | ||||||
|  | import { useTemplateRefsList, useVModel } from '@vueuse/core'; | ||||||
|  | import { NInputNumber } from 'naive-ui'; | ||||||
|  | import { nextTick } from 'vue'; | ||||||
|  | 
 | ||||||
|  | const props = defineProps<{ values: (number | null)[] }>(); | ||||||
|  | 
 | ||||||
|  | const emit = defineEmits(['update:values']); | ||||||
|  | 
 | ||||||
|  | const refs = useTemplateRefsList<typeof NInputNumber>(); | ||||||
|  | 
 | ||||||
|  | const values = useVModel(props, 'values', emit); | ||||||
|  | 
 | ||||||
|  | async function addValue() { | ||||||
|  |   values.value.push(null); | ||||||
|  |   await nextTick(); | ||||||
|  |   refs.value.at(-1)?.focus(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function onInputEnter(index: number) { | ||||||
|  |   if (index === values.value.length - 1) { | ||||||
|  |     addValue(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   refs.value.at(index + 1)?.focus(); | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2> |     <div v-for="(value, index) of values" :key="index" mb-2 flex flex-nowrap gap-2> | ||||||
|       <n-input-number |       <NInputNumber | ||||||
|         :ref="refs.set" |         :ref="refs.set" | ||||||
|         v-model:value="values[index]" |         v-model:value="values[index]" | ||||||
|         :show-button="false" |         :show-button="false" | ||||||
| @ -25,33 +55,3 @@ | |||||||
|     </c-button> |     </c-button> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { Trash, Plus } from '@vicons/tabler'; |  | ||||||
| import { useTemplateRefsList, useVModel } from '@vueuse/core'; |  | ||||||
| import { NInputNumber } from 'naive-ui'; |  | ||||||
| import { nextTick } from 'vue'; |  | ||||||
| 
 |  | ||||||
| const refs = useTemplateRefsList<typeof NInputNumber>(); |  | ||||||
| 
 |  | ||||||
| const props = defineProps<{ values: (number | null)[] }>(); |  | ||||||
| const emit = defineEmits(['update:values']); |  | ||||||
| const values = useVModel(props, 'values', emit); |  | ||||||
| 
 |  | ||||||
| async function addValue() { |  | ||||||
|   values.value.push(null); |  | ||||||
|   await nextTick(); |  | ||||||
|   refs.value.at(-1)?.focus(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function onInputEnter(index: number) { |  | ||||||
|   if (index === values.value.length - 1) { |  | ||||||
|     addValue(); |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   refs.value.at(index + 1)?.focus(); |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped></style> |  | ||||||
|  | |||||||
| @ -1,3 +1,85 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { | ||||||
|  |   chineseSimplifiedWordList, | ||||||
|  |   chineseTraditionalWordList, | ||||||
|  |   czechWordList, | ||||||
|  |   englishWordList, | ||||||
|  |   entropyToMnemonic, | ||||||
|  |   frenchWordList, | ||||||
|  |   generateEntropy, | ||||||
|  |   italianWordList, | ||||||
|  |   japaneseWordList, | ||||||
|  |   koreanWordList, | ||||||
|  |   mnemonicToEntropy, | ||||||
|  |   portugueseWordList, | ||||||
|  |   spanishWordList, | ||||||
|  | } from '@it-tools/bip39'; | ||||||
|  | import { Copy, Refresh } from '@vicons/tabler'; | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  | import { useValidation } from '@/composable/validation'; | ||||||
|  | import { isNotThrowing } from '@/utils/boolean'; | ||||||
|  | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
|  | 
 | ||||||
|  | const languages = { | ||||||
|  |   'English': englishWordList, | ||||||
|  |   'Chinese simplified': chineseSimplifiedWordList, | ||||||
|  |   'Chinese traditional': chineseTraditionalWordList, | ||||||
|  |   'Czech': czechWordList, | ||||||
|  |   'French': frenchWordList, | ||||||
|  |   'Italian': italianWordList, | ||||||
|  |   'Japanese': japaneseWordList, | ||||||
|  |   'Korean': koreanWordList, | ||||||
|  |   'Portuguese': portugueseWordList, | ||||||
|  |   'Spanish': spanishWordList, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const entropy = ref(generateEntropy()); | ||||||
|  | const passphraseInput = ref(''); | ||||||
|  | 
 | ||||||
|  | const language = ref<keyof typeof languages>('English'); | ||||||
|  | const passphrase = computed({ | ||||||
|  |   get() { | ||||||
|  |     return withDefaultOnError(() => entropyToMnemonic(entropy.value, languages[language.value]), passphraseInput.value); | ||||||
|  |   }, | ||||||
|  |   set(value: string) { | ||||||
|  |     passphraseInput.value = value; | ||||||
|  |     entropy.value = withDefaultOnError(() => mnemonicToEntropy(value, languages[language.value]), ''); | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const entropyValidation = useValidation({ | ||||||
|  |   source: entropy, | ||||||
|  |   rules: [ | ||||||
|  |     { | ||||||
|  |       validator: value => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0), | ||||||
|  |       message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       validator: value => /^[a-fA-F0-9]*$/.test(value), | ||||||
|  |       message: 'Entropy should be an hexadecimal string', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const mnemonicValidation = useValidation({ | ||||||
|  |   source: passphrase, | ||||||
|  |   rules: [ | ||||||
|  |     { | ||||||
|  |       validator: value => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])), | ||||||
|  |       message: 'Invalid mnemonic', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function refreshEntropy() { | ||||||
|  |   entropy.value = generateEntropy(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' }); | ||||||
|  | const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <n-grid cols="3" x-gap="12"> |     <n-grid cols="3" x-gap="12"> | ||||||
| @ -47,85 +129,3 @@ | |||||||
|     </n-form-item> |     </n-form-item> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useCopy } from '@/composable/copy'; |  | ||||||
| import { useValidation } from '@/composable/validation'; |  | ||||||
| import { isNotThrowing } from '@/utils/boolean'; |  | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; |  | ||||||
| import { |  | ||||||
|   chineseSimplifiedWordList, |  | ||||||
|   chineseTraditionalWordList, |  | ||||||
|   czechWordList, |  | ||||||
|   englishWordList, |  | ||||||
|   entropyToMnemonic, |  | ||||||
|   frenchWordList, |  | ||||||
|   generateEntropy, |  | ||||||
|   italianWordList, |  | ||||||
|   japaneseWordList, |  | ||||||
|   koreanWordList, |  | ||||||
|   mnemonicToEntropy, |  | ||||||
|   portugueseWordList, |  | ||||||
|   spanishWordList, |  | ||||||
| } from '@it-tools/bip39'; |  | ||||||
| import { Copy, Refresh } from '@vicons/tabler'; |  | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| 
 |  | ||||||
| const languages = { |  | ||||||
|   English: englishWordList, |  | ||||||
|   'Chinese simplified': chineseSimplifiedWordList, |  | ||||||
|   'Chinese traditional': chineseTraditionalWordList, |  | ||||||
|   Czech: czechWordList, |  | ||||||
|   French: frenchWordList, |  | ||||||
|   Italian: italianWordList, |  | ||||||
|   Japanese: japaneseWordList, |  | ||||||
|   Korean: koreanWordList, |  | ||||||
|   Portuguese: portugueseWordList, |  | ||||||
|   Spanish: spanishWordList, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| const entropy = ref(generateEntropy()); |  | ||||||
| const passphraseInput = ref(''); |  | ||||||
| 
 |  | ||||||
| const language = ref<keyof typeof languages>('English'); |  | ||||||
| const passphrase = computed({ |  | ||||||
|   get() { |  | ||||||
|     return withDefaultOnError(() => entropyToMnemonic(entropy.value, languages[language.value]), passphraseInput.value); |  | ||||||
|   }, |  | ||||||
|   set(value: string) { |  | ||||||
|     passphraseInput.value = value; |  | ||||||
|     entropy.value = withDefaultOnError(() => mnemonicToEntropy(value, languages[language.value]), ''); |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const entropyValidation = useValidation({ |  | ||||||
|   source: entropy, |  | ||||||
|   rules: [ |  | ||||||
|     { |  | ||||||
|       validator: (value) => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0), |  | ||||||
|       message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4', |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       validator: (value) => /^[a-fA-F0-9]*$/.test(value), |  | ||||||
|       message: 'Entropy should be an hexadecimal string', |  | ||||||
|     }, |  | ||||||
|   ], |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const mnemonicValidation = useValidation({ |  | ||||||
|   source: passphrase, |  | ||||||
|   rules: [ |  | ||||||
|     { |  | ||||||
|       validator: (value) => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])), |  | ||||||
|       message: 'Invalid mnemonic', |  | ||||||
|     }, |  | ||||||
|   ], |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| function refreshEntropy() { |  | ||||||
|   entropy.value = generateEntropy(); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' }); |  | ||||||
| const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' }); |  | ||||||
| </script> |  | ||||||
|  | |||||||
| @ -1,111 +1,9 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <c-card v-if="!isSupported"> Your browser does not support recording video from camera </c-card> |  | ||||||
| 
 |  | ||||||
|     <c-card v-else-if="!permissionGranted" text-center> |  | ||||||
|       You need to grant permission to use your camera and microphone |  | ||||||
| 
 |  | ||||||
|       <c-alert v-if="permissionCannotBePrompted" mt-4 text-left> |  | ||||||
|         Your browser has blocked permission request or does not support it. You need to grant permission manually in |  | ||||||
|         your browser settings (usually the lock icon in the address bar). |  | ||||||
|       </c-alert> |  | ||||||
| 
 |  | ||||||
|       <div v-else mt-4 flex justify-center> |  | ||||||
|         <c-button @click="requestPermissions">Grant permission</c-button> |  | ||||||
|       </div> |  | ||||||
|     </c-card> |  | ||||||
| 
 |  | ||||||
|     <c-card v-else> |  | ||||||
|       <div flex gap-2> |  | ||||||
|         <div flex-1> |  | ||||||
|           <div>Video</div> |  | ||||||
|           <n-select |  | ||||||
|             v-model:value="currentCamera" |  | ||||||
|             :options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))" |  | ||||||
|             placeholder="Select camera" |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|         <div v-if="currentMicrophone && microphones.length > 0" flex-1> |  | ||||||
|           <div>Audio</div> |  | ||||||
|           <n-select |  | ||||||
|             v-model:value="currentMicrophone" |  | ||||||
|             :options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))" |  | ||||||
|             placeholder="Select microphone" |  | ||||||
|           /> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <div v-if="!isMediaStreamAvailable" mt-3 flex justify-center> |  | ||||||
|         <c-button type="primary" @click="start">Start webcam</c-button> |  | ||||||
|       </div> |  | ||||||
| 
 |  | ||||||
|       <div v-else> |  | ||||||
|         <div my-2> |  | ||||||
|           <video ref="video" autoplay controls playsinline max-h-full w-full /> |  | ||||||
|         </div> |  | ||||||
| 
 |  | ||||||
|         <div flex items-center justify-between gap-2> |  | ||||||
|           <c-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot"> |  | ||||||
|             <span mr-2> <icon-mdi-camera /></span> |  | ||||||
|             Take screenshot |  | ||||||
|           </c-button> |  | ||||||
| 
 |  | ||||||
|           <div v-if="isRecordingSupported" flex justify-center gap-2> |  | ||||||
|             <c-button v-if="recordingState === 'stopped'" @click="startRecording"> |  | ||||||
|               <span mr-2> <icon-mdi-video /></span> |  | ||||||
|               Start recording |  | ||||||
|             </c-button> |  | ||||||
| 
 |  | ||||||
|             <c-button v-if="recordingState === 'recording'" @click="pauseRecording"> |  | ||||||
|               <span mr-2> <icon-mdi-pause /></span> |  | ||||||
|               Pause |  | ||||||
|             </c-button> |  | ||||||
| 
 |  | ||||||
|             <c-button v-if="recordingState === 'paused'" @click="resumeRecording"> |  | ||||||
|               <span mr-2> <icon-mdi-play /></span> |  | ||||||
|               Resume |  | ||||||
|             </c-button> |  | ||||||
| 
 |  | ||||||
|             <c-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording"> |  | ||||||
|               <span mr-2> <icon-mdi-record /></span> |  | ||||||
|               Stop |  | ||||||
|             </c-button> |  | ||||||
|           </div> |  | ||||||
|           <div v-else italic op-60>Video recording is not supported in your browser</div> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </c-card> |  | ||||||
| 
 |  | ||||||
|     <div grid grid-cols-2 mt-5 gap-2> |  | ||||||
|       <c-card v-for="({ type, value, createdAt }, index) in medias" :key="index"> |  | ||||||
|         <img v-if="type === 'image'" :src="value" max-h-full w-full alt="screenshot" /> |  | ||||||
| 
 |  | ||||||
|         <video v-else :src="value" controls max-h-full w-full /> |  | ||||||
| 
 |  | ||||||
|         <div flex items-center justify-between> |  | ||||||
|           <div font-bold>{{ type === 'image' ? 'Screenshot' : 'Video' }}</div> |  | ||||||
| 
 |  | ||||||
|           <div flex gap-2> |  | ||||||
|             <c-button @click="downloadMedia({ type, value, createdAt })"> |  | ||||||
|               <icon-mdi-download /> |  | ||||||
|             </c-button> |  | ||||||
| 
 |  | ||||||
|             <c-button @click="medias = medias.filter((_ignored, i) => i !== index)"> |  | ||||||
|               <icon-mdi-delete-outline /> |  | ||||||
|             </c-button> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       </c-card> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| 
 | 
 | ||||||
| import { useMediaRecorder } from './useMediaRecorder'; | import { useMediaRecorder } from './useMediaRecorder'; | ||||||
| 
 | 
 | ||||||
| type Media = { type: 'image' | 'video'; value: string; createdAt: Date }; | interface Media { type: 'image' | 'video'; value: string; createdAt: Date } | ||||||
| 
 | 
 | ||||||
| const { | const { | ||||||
|   videoInputs: cameras, |   videoInputs: cameras, | ||||||
| @ -156,19 +54,19 @@ onRecordAvailable((value) => { | |||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| function refreshCurrentDevices() { | function refreshCurrentDevices() { | ||||||
|   console.log('refreshCurrentDevices'); |   if (_.isNil(currentCamera) || !cameras.value.find(i => i.deviceId === currentCamera.value)) { | ||||||
| 
 |  | ||||||
|   if (_.isNil(currentCamera) || !cameras.value.find((i) => i.deviceId === currentCamera.value)) { |  | ||||||
|     currentCamera.value = cameras.value[0]?.deviceId; |     currentCamera.value = cameras.value[0]?.deviceId; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   if (_.isNil(microphones) || !microphones.value.find((i) => i.deviceId === currentMicrophone.value)) { |   if (_.isNil(microphones) || !microphones.value.find(i => i.deviceId === currentMicrophone.value)) { | ||||||
|     currentMicrophone.value = microphones.value[0]?.deviceId; |     currentMicrophone.value = microphones.value[0]?.deviceId; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function takeScreenshot() { | function takeScreenshot() { | ||||||
|   if (!video.value) return; |   if (!video.value) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   const canvas = document.createElement('canvas'); |   const canvas = document.createElement('canvas'); | ||||||
|   canvas.width = video.value.videoWidth; |   canvas.width = video.value.videoWidth; | ||||||
| @ -180,13 +78,16 @@ function takeScreenshot() { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| watchEffect(() => { | watchEffect(() => { | ||||||
|   if (video.value && stream.value) video.value.srcObject = stream.value; |   if (video.value && stream.value) { | ||||||
|  |     video.value.srcObject = stream.value; | ||||||
|  |   } | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| async function requestPermissions() { | async function requestPermissions() { | ||||||
|   try { |   try { | ||||||
|     await ensurePermissions(); |     await ensurePermissions(); | ||||||
|   } catch (e) { |   } | ||||||
|  |   catch (e) { | ||||||
|     permissionCannotBePrompted.value = true; |     permissionCannotBePrompted.value = true; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -199,4 +100,114 @@ function downloadMedia({ type, value, createdAt }: Media) { | |||||||
| } | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="less" scoped></style> | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-card v-if="!isSupported"> | ||||||
|  |       Your browser does not support recording video from camera | ||||||
|  |     </c-card> | ||||||
|  | 
 | ||||||
|  |     <c-card v-else-if="!permissionGranted" text-center> | ||||||
|  |       You need to grant permission to use your camera and microphone | ||||||
|  | 
 | ||||||
|  |       <c-alert v-if="permissionCannotBePrompted" mt-4 text-left> | ||||||
|  |         Your browser has blocked permission request or does not support it. You need to grant permission manually in | ||||||
|  |         your browser settings (usually the lock icon in the address bar). | ||||||
|  |       </c-alert> | ||||||
|  | 
 | ||||||
|  |       <div v-else mt-4 flex justify-center> | ||||||
|  |         <c-button @click="requestPermissions"> | ||||||
|  |           Grant permission | ||||||
|  |         </c-button> | ||||||
|  |       </div> | ||||||
|  |     </c-card> | ||||||
|  | 
 | ||||||
|  |     <c-card v-else> | ||||||
|  |       <div flex gap-2> | ||||||
|  |         <div flex-1> | ||||||
|  |           <div>Video</div> | ||||||
|  |           <n-select | ||||||
|  |             v-model:value="currentCamera" | ||||||
|  |             :options="cameras.map(({ deviceId, label }) => ({ value: deviceId, label }))" | ||||||
|  |             placeholder="Select camera" | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="currentMicrophone && microphones.length > 0" flex-1> | ||||||
|  |           <div>Audio</div> | ||||||
|  |           <n-select | ||||||
|  |             v-model:value="currentMicrophone" | ||||||
|  |             :options="microphones.map(({ deviceId, label }) => ({ value: deviceId, label }))" | ||||||
|  |             placeholder="Select microphone" | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <div v-if="!isMediaStreamAvailable" mt-3 flex justify-center> | ||||||
|  |         <c-button type="primary" @click="start"> | ||||||
|  |           Start webcam | ||||||
|  |         </c-button> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <div v-else> | ||||||
|  |         <div my-2> | ||||||
|  |           <video ref="video" autoplay controls playsinline max-h-full w-full /> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <div flex items-center justify-between gap-2> | ||||||
|  |           <c-button :disabled="!isMediaStreamAvailable" @click="takeScreenshot"> | ||||||
|  |             <span mr-2> <icon-mdi-camera /></span> | ||||||
|  |             Take screenshot | ||||||
|  |           </c-button> | ||||||
|  | 
 | ||||||
|  |           <div v-if="isRecordingSupported" flex justify-center gap-2> | ||||||
|  |             <c-button v-if="recordingState === 'stopped'" @click="startRecording"> | ||||||
|  |               <span mr-2> <icon-mdi-video /></span> | ||||||
|  |               Start recording | ||||||
|  |             </c-button> | ||||||
|  | 
 | ||||||
|  |             <c-button v-if="recordingState === 'recording'" @click="pauseRecording"> | ||||||
|  |               <span mr-2> <icon-mdi-pause /></span> | ||||||
|  |               Pause | ||||||
|  |             </c-button> | ||||||
|  | 
 | ||||||
|  |             <c-button v-if="recordingState === 'paused'" @click="resumeRecording"> | ||||||
|  |               <span mr-2> <icon-mdi-play /></span> | ||||||
|  |               Resume | ||||||
|  |             </c-button> | ||||||
|  | 
 | ||||||
|  |             <c-button v-if="recordingState !== 'stopped'" type="error" @click="stopRecording"> | ||||||
|  |               <span mr-2> <icon-mdi-record /></span> | ||||||
|  |               Stop | ||||||
|  |             </c-button> | ||||||
|  |           </div> | ||||||
|  |           <div v-else italic op-60> | ||||||
|  |             Video recording is not supported in your browser | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </c-card> | ||||||
|  | 
 | ||||||
|  |     <div grid grid-cols-2 mt-5 gap-2> | ||||||
|  |       <c-card v-for="({ type, value, createdAt }, index) in medias" :key="index"> | ||||||
|  |         <img v-if="type === 'image'" :src="value" max-h-full w-full alt="screenshot"> | ||||||
|  | 
 | ||||||
|  |         <video v-else :src="value" controls max-h-full w-full /> | ||||||
|  | 
 | ||||||
|  |         <div flex items-center justify-between> | ||||||
|  |           <div font-bold> | ||||||
|  |             {{ type === 'image' ? 'Screenshot' : 'Video' }} | ||||||
|  |           </div> | ||||||
|  | 
 | ||||||
|  |           <div flex gap-2> | ||||||
|  |             <c-button @click="downloadMedia({ type, value, createdAt })"> | ||||||
|  |               <icon-mdi-download /> | ||||||
|  |             </c-button> | ||||||
|  | 
 | ||||||
|  |             <c-button @click="medias = medias.filter((_ignored, i) => i !== index)"> | ||||||
|  |               <icon-mdi-delete-outline /> | ||||||
|  |             </c-button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </c-card> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,15 +1,15 @@ | |||||||
| import { computed, ref, type Ref } from 'vue'; | import { type Ref, computed, ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
| export { useMediaRecorder }; | export { useMediaRecorder }; | ||||||
| 
 | 
 | ||||||
| function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): { | function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): { | ||||||
|   isRecordingSupported: Ref<boolean>; |   isRecordingSupported: Ref<boolean> | ||||||
|   recordingState: Ref<'stopped' | 'recording' | 'paused'>; |   recordingState: Ref<'stopped' | 'recording' | 'paused'> | ||||||
|   startRecording: () => void; |   startRecording: () => void | ||||||
|   stopRecording: () => void; |   stopRecording: () => void | ||||||
|   pauseRecording: () => void; |   pauseRecording: () => void | ||||||
|   resumeRecording: () => void; |   resumeRecording: () => void | ||||||
|   onRecordAvailable: (cb: (url: string) => void) => void; |   onRecordAvailable: (cb: (url: string) => void) => void | ||||||
| } { | } { | ||||||
|   const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm')); |   const isRecordingSupported = computed(() => MediaRecorder.isTypeSupported('video/webm')); | ||||||
|   const mediaRecorder = ref<MediaRecorder | null>(null); |   const mediaRecorder = ref<MediaRecorder | null>(null); | ||||||
| @ -17,10 +17,23 @@ function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): | |||||||
|   const recordAvailable = createEventHook(); |   const recordAvailable = createEventHook(); | ||||||
|   const recordingState = ref<'stopped' | 'recording' | 'paused'>('stopped'); |   const recordingState = ref<'stopped' | 'recording' | 'paused'>('stopped'); | ||||||
| 
 | 
 | ||||||
|  |   const createVideo = () => { | ||||||
|  |     const blob = new Blob(recordedChunks.value, { type: 'video/webm' }); | ||||||
|  |     const url = URL.createObjectURL(blob); | ||||||
|  |     recordedChunks.value = []; | ||||||
|  |     return url; | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   const startRecording = () => { |   const startRecording = () => { | ||||||
|     if (!isRecordingSupported.value) return; |     if (!isRecordingSupported.value) { | ||||||
|     if (!stream.value) return; |       return; | ||||||
|     if (recordingState.value !== 'stopped') return; |     } | ||||||
|  |     if (!stream.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (recordingState.value !== 'stopped') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' }); |     mediaRecorder.value = new MediaRecorder(stream.value, { mimeType: 'video/webm' }); | ||||||
| 
 | 
 | ||||||
| @ -34,47 +47,60 @@ function useMediaRecorder({ stream }: { stream: Ref<MediaStream | undefined> }): | |||||||
|       recordAvailable.trigger(createVideo()); |       recordAvailable.trigger(createVideo()); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     if (mediaRecorder.value.state !== 'inactive') return; |     if (mediaRecorder.value.state !== 'inactive') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     mediaRecorder.value.start(); |     mediaRecorder.value.start(); | ||||||
|     recordingState.value = 'recording'; |     recordingState.value = 'recording'; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const stopRecording = () => { |   const stopRecording = () => { | ||||||
|     if (!isRecordingSupported.value) return; |     if (!isRecordingSupported.value) { | ||||||
|     if (!mediaRecorder.value) return; |       return; | ||||||
|     if (recordingState.value === 'stopped') return; |     } | ||||||
|  |     if (!mediaRecorder.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (recordingState.value === 'stopped') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     mediaRecorder.value.stop(); |     mediaRecorder.value.stop(); | ||||||
|     recordingState.value = 'stopped'; |     recordingState.value = 'stopped'; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const pauseRecording = () => { |   const pauseRecording = () => { | ||||||
|     if (!isRecordingSupported.value) return; |     if (!isRecordingSupported.value) { | ||||||
|     if (!mediaRecorder.value) return; |       return; | ||||||
|     if (recordingState.value !== 'recording') return; |     } | ||||||
|  |     if (!mediaRecorder.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (recordingState.value !== 'recording') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     mediaRecorder.value.pause(); |     mediaRecorder.value.pause(); | ||||||
|     recordingState.value = 'paused'; |     recordingState.value = 'paused'; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const resumeRecording = () => { |   const resumeRecording = () => { | ||||||
|     if (!isRecordingSupported.value) return; |     if (!isRecordingSupported.value) { | ||||||
|     if (!mediaRecorder.value) return; |       return; | ||||||
|  |     } | ||||||
|  |     if (!mediaRecorder.value) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     if (recordingState.value !== 'paused') return; |     if (recordingState.value !== 'paused') { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     mediaRecorder.value.resume(); |     mediaRecorder.value.resume(); | ||||||
|     recordingState.value = 'recording'; |     recordingState.value = 'recording'; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const createVideo = () => { |  | ||||||
|     const blob = new Blob(recordedChunks.value, { type: 'video/webm' }); |  | ||||||
|     const url = URL.createObjectURL(blob); |  | ||||||
|     recordedChunks.value = []; |  | ||||||
|     return url; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   return { |   return { | ||||||
|     isRecordingSupported, |     isRecordingSupported, | ||||||
|     startRecording, |     startRecording, | ||||||
|  | |||||||
| @ -1,55 +1,3 @@ | |||||||
| <template> |  | ||||||
|   <c-card> |  | ||||||
|     <n-form label-width="120" label-placement="left" :show-feedback="false"> |  | ||||||
|       <c-input-text |  | ||||||
|         v-model:value="input" |  | ||||||
|         label="Your string" |  | ||||||
|         label-position="left" |  | ||||||
|         label-width="120px" |  | ||||||
|         label-align="right" |  | ||||||
|         placeholder="Your string..." |  | ||||||
|         raw-text |  | ||||||
|       /> |  | ||||||
| 
 |  | ||||||
|       <n-divider /> |  | ||||||
| 
 |  | ||||||
|       <n-form-item label="Camelcase:"> |  | ||||||
|         <input-copyable :value="camelCase(input, baseConfig)" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="Capitalcase:"> |  | ||||||
|         <input-copyable :value="capitalCase(input, baseConfig)" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="Constantcase:"> |  | ||||||
|         <input-copyable :value="constantCase(input, baseConfig)" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="Dotcase:"> |  | ||||||
|         <input-copyable :value="dotCase(input, baseConfig)" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="Headercase:"> |  | ||||||
|         <input-copyable :value="headerCase(input, baseConfig)" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="Nocase:"> |  | ||||||
|         <input-copyable :value="noCase(input, baseConfig)" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="Paramcase:"> |  | ||||||
|         <input-copyable :value="paramCase(input, baseConfig)" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="Pascalcase:"> |  | ||||||
|         <input-copyable :value="pascalCase(input, baseConfig)" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="Pathcase:"> |  | ||||||
|         <input-copyable :value="pathCase(input, baseConfig)" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="Sentencecase:"> |  | ||||||
|         <input-copyable :value="sentenceCase(input, baseConfig)" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="Snakecase:"> |  | ||||||
|         <input-copyable :value="snakeCase(input, baseConfig)" /> |  | ||||||
|       </n-form-item> |  | ||||||
|     </n-form> |  | ||||||
|   </c-card> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| import { | import { | ||||||
| @ -74,6 +22,58 @@ const baseConfig = { | |||||||
| const input = ref('lorem ipsum dolor sit amet'); | const input = ref('lorem ipsum dolor sit amet'); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | <template> | ||||||
|  |   <c-card> | ||||||
|  |     <n-form label-width="120" label-placement="left" :show-feedback="false"> | ||||||
|  |       <c-input-text | ||||||
|  |         v-model:value="input" | ||||||
|  |         label="Your string" | ||||||
|  |         label-position="left" | ||||||
|  |         label-width="120px" | ||||||
|  |         label-align="right" | ||||||
|  |         placeholder="Your string..." | ||||||
|  |         raw-text | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|  |       <n-divider /> | ||||||
|  | 
 | ||||||
|  |       <n-form-item label="Camelcase:"> | ||||||
|  |         <InputCopyable :value="camelCase(input, baseConfig)" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="Capitalcase:"> | ||||||
|  |         <InputCopyable :value="capitalCase(input, baseConfig)" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="Constantcase:"> | ||||||
|  |         <InputCopyable :value="constantCase(input, baseConfig)" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="Dotcase:"> | ||||||
|  |         <InputCopyable :value="dotCase(input, baseConfig)" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="Headercase:"> | ||||||
|  |         <InputCopyable :value="headerCase(input, baseConfig)" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="Nocase:"> | ||||||
|  |         <InputCopyable :value="noCase(input, baseConfig)" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="Paramcase:"> | ||||||
|  |         <InputCopyable :value="paramCase(input, baseConfig)" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="Pascalcase:"> | ||||||
|  |         <InputCopyable :value="pascalCase(input, baseConfig)" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="Pathcase:"> | ||||||
|  |         <InputCopyable :value="pathCase(input, baseConfig)" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="Sentencecase:"> | ||||||
|  |         <InputCopyable :value="sentenceCase(input, baseConfig)" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="Snakecase:"> | ||||||
|  |         <InputCopyable :value="snakeCase(input, baseConfig)" /> | ||||||
|  |       </n-form-item> | ||||||
|  |     </n-form> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .n-form-item { | .n-form-item { | ||||||
|   margin: 5px 0; |   margin: 5px 0; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { expect, describe, it } from 'vitest'; | import { describe, expect, it } from 'vitest'; | ||||||
| import { computeChmodOctalRepresentation } from './chmod-calculator.service'; | import { computeChmodOctalRepresentation } from './chmod-calculator.service'; | ||||||
| 
 | 
 | ||||||
| describe('chmod-calculator', () => { | describe('chmod-calculator', () => { | ||||||
|  | |||||||
| @ -1,38 +1,8 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table"> |  | ||||||
|       <thead> |  | ||||||
|         <tr> |  | ||||||
|           <th class="text-center" scope="col"></th> |  | ||||||
|           <th class="text-center" scope="col">Owner (u)</th> |  | ||||||
|           <th class="text-center" scope="col">Group (g)</th> |  | ||||||
|           <th class="text-center" scope="col">Public (o)</th> |  | ||||||
|         </tr> |  | ||||||
|       </thead> |  | ||||||
|       <tbody> |  | ||||||
|         <tr v-for="{ scope, title } of scopes" :key="scope"> |  | ||||||
|           <td class="line-header">{{ title }}</td> |  | ||||||
|           <td v-for="group of groups" :key="group" class="text-center"> |  | ||||||
|             <!-- <n-switch v-model:value="permissions[group][scope]" /> --> |  | ||||||
|             <n-checkbox v-model:checked="permissions[group][scope]" size="large" /> |  | ||||||
|           </td> |  | ||||||
|         </tr> |  | ||||||
|       </tbody> |  | ||||||
|     </n-table> |  | ||||||
| 
 |  | ||||||
|     <div class="octal-result"> |  | ||||||
|       {{ octal }} |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <input-copyable :value="`chmod ${octal} path`" readonly /> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useThemeVars } from 'naive-ui'; | import { useThemeVars } from 'naive-ui'; | ||||||
| import { computed, ref } from 'vue'; | import { computed, ref } from 'vue'; | ||||||
| import { computeChmodOctalRepresentation } from './chmod-calculator.service'; |  | ||||||
| import InputCopyable from '../../components/InputCopyable.vue'; | import InputCopyable from '../../components/InputCopyable.vue'; | ||||||
|  | import { computeChmodOctalRepresentation } from './chmod-calculator.service'; | ||||||
| 
 | 
 | ||||||
| import type { Group, Scope } from './chmod-calculator.types'; | import type { Group, Scope } from './chmod-calculator.types'; | ||||||
| 
 | 
 | ||||||
| @ -54,6 +24,44 @@ const permissions = ref({ | |||||||
| const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value })); | const octal = computed(() => computeChmodOctalRepresentation({ permissions: permissions.value })); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <n-table :bordered="false" :bottom-bordered="false" single-column class="permission-table"> | ||||||
|  |       <thead> | ||||||
|  |         <tr> | ||||||
|  |           <th class="text-center" scope="col" /> | ||||||
|  |           <th class="text-center" scope="col"> | ||||||
|  |             Owner (u) | ||||||
|  |           </th> | ||||||
|  |           <th class="text-center" scope="col"> | ||||||
|  |             Group (g) | ||||||
|  |           </th> | ||||||
|  |           <th class="text-center" scope="col"> | ||||||
|  |             Public (o) | ||||||
|  |           </th> | ||||||
|  |         </tr> | ||||||
|  |       </thead> | ||||||
|  |       <tbody> | ||||||
|  |         <tr v-for="{ scope, title } of scopes" :key="scope"> | ||||||
|  |           <td class="line-header"> | ||||||
|  |             {{ title }} | ||||||
|  |           </td> | ||||||
|  |           <td v-for="group of groups" :key="group" class="text-center"> | ||||||
|  |             <!-- <n-switch v-model:value="permissions[group][scope]" /> --> | ||||||
|  |             <n-checkbox v-model:checked="permissions[group][scope]" size="large" /> | ||||||
|  |           </td> | ||||||
|  |         </tr> | ||||||
|  |       </tbody> | ||||||
|  |     </n-table> | ||||||
|  | 
 | ||||||
|  |     <div class="octal-result"> | ||||||
|  |       {{ octal }} | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <InputCopyable :value="`chmod ${octal} path`" readonly /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .octal-result { | .octal-result { | ||||||
|   text-align: center; |   text-align: center; | ||||||
|  | |||||||
| @ -1,17 +1,3 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <c-card> |  | ||||||
|       <div class="duration">{{ formatMs(counter) }}</div> |  | ||||||
|     </c-card> |  | ||||||
|     <div mt-5 flex justify-center gap-3> |  | ||||||
|       <c-button v-if="!isRunning" type="primary" @click="resume">Start</c-button> |  | ||||||
|       <c-button v-else type="warning" @click="pause">Stop</c-button> |  | ||||||
| 
 |  | ||||||
|       <c-button @click="counter = 0">Reset</c-button> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useRafFn } from '@vueuse/core'; | import { useRafFn } from '@vueuse/core'; | ||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| @ -42,6 +28,28 @@ function pause() { | |||||||
| } | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-card> | ||||||
|  |       <div class="duration"> | ||||||
|  |         {{ formatMs(counter) }} | ||||||
|  |       </div> | ||||||
|  |     </c-card> | ||||||
|  |     <div mt-5 flex justify-center gap-3> | ||||||
|  |       <c-button v-if="!isRunning" type="primary" @click="resume"> | ||||||
|  |         Start | ||||||
|  |       </c-button> | ||||||
|  |       <c-button v-else type="warning" @click="pause"> | ||||||
|  |         Stop | ||||||
|  |       </c-button> | ||||||
|  | 
 | ||||||
|  |       <c-button @click="counter = 0"> | ||||||
|  |         Reset | ||||||
|  |       </c-button> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .duration { | .duration { | ||||||
|   text-align: center; |   text-align: center; | ||||||
|  | |||||||
| @ -1,38 +1,3 @@ | |||||||
| <template> |  | ||||||
|   <c-card> |  | ||||||
|     <n-form label-width="100" label-placement="left"> |  | ||||||
|       <n-form-item label="color picker:"> |  | ||||||
|         <n-color-picker |  | ||||||
|           v-model:value="hex" |  | ||||||
|           placement="bottom-end" |  | ||||||
|           @update:value="(v: string) => onInputUpdated(v, 'hex')" |  | ||||||
|         /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="color name:"> |  | ||||||
|         <input-copyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="hex:"> |  | ||||||
|         <input-copyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="rgb:"> |  | ||||||
|         <input-copyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="hsl:"> |  | ||||||
|         <input-copyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="hwb:"> |  | ||||||
|         <input-copyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="lch:"> |  | ||||||
|         <input-copyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" /> |  | ||||||
|       </n-form-item> |  | ||||||
|       <n-form-item label="cmyk:"> |  | ||||||
|         <input-copyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" /> |  | ||||||
|       </n-form-item> |  | ||||||
|     </n-form> |  | ||||||
|   </c-card> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { ref } from 'vue'; | import { ref } from 'vue'; | ||||||
| import { colord, extend } from 'colord'; | import { colord, extend } from 'colord'; | ||||||
| @ -57,17 +22,67 @@ function onInputUpdated(value: string, omit: string) { | |||||||
|   try { |   try { | ||||||
|     const color = colord(value); |     const color = colord(value); | ||||||
| 
 | 
 | ||||||
|     if (omit !== 'name') name.value = color.toName({ closest: true }) ?? ''; |     if (omit !== 'name') { | ||||||
|     if (omit !== 'hex') hex.value = color.toHex(); |       name.value = color.toName({ closest: true }) ?? ''; | ||||||
|     if (omit !== 'rgb') rgb.value = color.toRgbString(); |     } | ||||||
|     if (omit !== 'hsl') hsl.value = color.toHslString(); |     if (omit !== 'hex') { | ||||||
|     if (omit !== 'hwb') hwb.value = color.toHwbString(); |       hex.value = color.toHex(); | ||||||
|     if (omit !== 'cmyk') cmyk.value = color.toCmykString(); |     } | ||||||
|     if (omit !== 'lch') lch.value = color.toLchString(); |     if (omit !== 'rgb') { | ||||||
|   } catch { |       rgb.value = color.toRgbString(); | ||||||
|  |     } | ||||||
|  |     if (omit !== 'hsl') { | ||||||
|  |       hsl.value = color.toHslString(); | ||||||
|  |     } | ||||||
|  |     if (omit !== 'hwb') { | ||||||
|  |       hwb.value = color.toHwbString(); | ||||||
|  |     } | ||||||
|  |     if (omit !== 'cmyk') { | ||||||
|  |       cmyk.value = color.toCmykString(); | ||||||
|  |     } | ||||||
|  |     if (omit !== 'lch') { | ||||||
|  |       lch.value = color.toLchString(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   catch { | ||||||
|     // |     // | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| onInputUpdated(hex.value, 'hex'); | onInputUpdated(hex.value, 'hex'); | ||||||
| </script> | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <c-card> | ||||||
|  |     <n-form label-width="100" label-placement="left"> | ||||||
|  |       <n-form-item label="color picker:"> | ||||||
|  |         <n-color-picker | ||||||
|  |           v-model:value="hex" | ||||||
|  |           placement="bottom-end" | ||||||
|  |           @update:value="(v: string) => onInputUpdated(v, 'hex')" | ||||||
|  |         /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="color name:"> | ||||||
|  |         <InputCopyable v-model:value="name" @update:value="(v: string) => onInputUpdated(v, 'name')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="hex:"> | ||||||
|  |         <InputCopyable v-model:value="hex" @update:value="(v: string) => onInputUpdated(v, 'hex')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="rgb:"> | ||||||
|  |         <InputCopyable v-model:value="rgb" @update:value="(v: string) => onInputUpdated(v, 'rgb')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="hsl:"> | ||||||
|  |         <InputCopyable v-model:value="hsl" @update:value="(v: string) => onInputUpdated(v, 'hsl')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="hwb:"> | ||||||
|  |         <InputCopyable v-model:value="hwb" @update:value="(v: string) => onInputUpdated(v, 'hwb')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="lch:"> | ||||||
|  |         <InputCopyable v-model:value="lch" @update:value="(v: string) => onInputUpdated(v, 'lch')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |       <n-form-item label="cmyk:"> | ||||||
|  |         <InputCopyable v-model:value="cmyk" @update:value="(v: string) => onInputUpdated(v, 'cmyk')" /> | ||||||
|  |       </n-form-item> | ||||||
|  |     </n-form> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,89 +1,3 @@ | |||||||
| <template> |  | ||||||
|   <c-card> |  | ||||||
|     <div mx-auto max-w-sm> |  | ||||||
|       <c-input-text |  | ||||||
|         v-model:value="cron" |  | ||||||
|         size="large" |  | ||||||
|         placeholder="* * * * *" |  | ||||||
|         :validation-rules="cronValidationRules" |  | ||||||
|         mb-3 |  | ||||||
|       /> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <div class="cron-string"> |  | ||||||
|       {{ cronString }} |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <n-divider /> |  | ||||||
| 
 |  | ||||||
|     <div flex justify-center> |  | ||||||
|       <n-form :show-feedback="false" label-width="170" label-placement="left"> |  | ||||||
|         <n-form-item label="Verbose"> |  | ||||||
|           <n-switch v-model:value="cronstrueConfig.verbose" /> |  | ||||||
|         </n-form-item> |  | ||||||
|         <n-form-item label="Use 24 hour time format"> |  | ||||||
|           <n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" /> |  | ||||||
|         </n-form-item> |  | ||||||
|         <n-form-item label="Days start at 0"> |  | ||||||
|           <n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" /> |  | ||||||
|         </n-form-item> |  | ||||||
|       </n-form> |  | ||||||
|     </div> |  | ||||||
|   </c-card> |  | ||||||
|   <c-card> |  | ||||||
|     <pre> |  | ||||||
| ┌──────────── [optional] seconds (0 - 59) |  | ||||||
| | ┌────────── minute (0 - 59) |  | ||||||
| | | ┌──────── hour (0 - 23) |  | ||||||
| | | | ┌────── day of month (1 - 31) |  | ||||||
| | | | | ┌──── month (1 - 12) OR jan,feb,mar,apr ... |  | ||||||
| | | | | | ┌── day of week (0 - 6, sunday=0) OR sun,mon ... |  | ||||||
| | | | | | | |  | ||||||
| * * * * * * command</pre |  | ||||||
|     > |  | ||||||
| 
 |  | ||||||
|     <div v-if="styleStore.isSmallScreen"> |  | ||||||
|       <c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none> |  | ||||||
|         <div> |  | ||||||
|           Symbol: <strong>{{ symbol }}</strong> |  | ||||||
|         </div> |  | ||||||
|         <div> |  | ||||||
|           Meaning: <strong>{{ meaning }}</strong> |  | ||||||
|         </div> |  | ||||||
|         <div> |  | ||||||
|           Example: |  | ||||||
|           <strong |  | ||||||
|             ><code>{{ example }}</code></strong |  | ||||||
|           > |  | ||||||
|         </div> |  | ||||||
|         <div> |  | ||||||
|           Equivalent: <strong>{{ equivalent }}</strong> |  | ||||||
|         </div> |  | ||||||
|       </c-card> |  | ||||||
|     </div> |  | ||||||
|     <n-table v-else size="small"> |  | ||||||
|       <thead> |  | ||||||
|         <tr> |  | ||||||
|           <th class="text-left" scope="col">Symbol</th> |  | ||||||
|           <th class="text-left" scope="col">Meaning</th> |  | ||||||
|           <th class="text-left" scope="col">Example</th> |  | ||||||
|           <th class="text-left" scope="col">Equivalent</th> |  | ||||||
|         </tr> |  | ||||||
|       </thead> |  | ||||||
|       <tbody> |  | ||||||
|         <tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol"> |  | ||||||
|           <td>{{ symbol }}</td> |  | ||||||
|           <td>{{ meaning }}</td> |  | ||||||
|           <td> |  | ||||||
|             <code>{{ example }}</code> |  | ||||||
|           </td> |  | ||||||
|           <td>{{ equivalent }}</td> |  | ||||||
|         </tr> |  | ||||||
|       </tbody> |  | ||||||
|     </n-table> |  | ||||||
|   </c-card> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import cronstrue from 'cronstrue'; | import cronstrue from 'cronstrue'; | ||||||
| import { isValidCron } from 'cron-validator'; | import { isValidCron } from 'cron-validator'; | ||||||
| @ -194,6 +108,97 @@ const cronValidationRules = [ | |||||||
| ]; | ]; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | <template> | ||||||
|  |   <c-card> | ||||||
|  |     <div mx-auto max-w-sm> | ||||||
|  |       <c-input-text | ||||||
|  |         v-model:value="cron" | ||||||
|  |         size="large" | ||||||
|  |         placeholder="* * * * *" | ||||||
|  |         :validation-rules="cronValidationRules" | ||||||
|  |         mb-3 | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div class="cron-string"> | ||||||
|  |       {{ cronString }} | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <n-divider /> | ||||||
|  | 
 | ||||||
|  |     <div flex justify-center> | ||||||
|  |       <n-form :show-feedback="false" label-width="170" label-placement="left"> | ||||||
|  |         <n-form-item label="Verbose"> | ||||||
|  |           <n-switch v-model:value="cronstrueConfig.verbose" /> | ||||||
|  |         </n-form-item> | ||||||
|  |         <n-form-item label="Use 24 hour time format"> | ||||||
|  |           <n-switch v-model:value="cronstrueConfig.use24HourTimeFormat" /> | ||||||
|  |         </n-form-item> | ||||||
|  |         <n-form-item label="Days start at 0"> | ||||||
|  |           <n-switch v-model:value="cronstrueConfig.dayOfWeekStartIndexZero" /> | ||||||
|  |         </n-form-item> | ||||||
|  |       </n-form> | ||||||
|  |     </div> | ||||||
|  |   </c-card> | ||||||
|  |   <c-card> | ||||||
|  |     <pre> | ||||||
|  | ┌──────────── [optional] seconds (0 - 59) | ||||||
|  | | ┌────────── minute (0 - 59) | ||||||
|  | | | ┌──────── hour (0 - 23) | ||||||
|  | | | | ┌────── day of month (1 - 31) | ||||||
|  | | | | | ┌──── month (1 - 12) OR jan,feb,mar,apr ... | ||||||
|  | | | | | | ┌── day of week (0 - 6, sunday=0) OR sun,mon ... | ||||||
|  | | | | | | | | ||||||
|  | * * * * * * command</pre> | ||||||
|  | 
 | ||||||
|  |     <div v-if="styleStore.isSmallScreen"> | ||||||
|  |       <c-card v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol" mb-3 important:border-none> | ||||||
|  |         <div> | ||||||
|  |           Symbol: <strong>{{ symbol }}</strong> | ||||||
|  |         </div> | ||||||
|  |         <div> | ||||||
|  |           Meaning: <strong>{{ meaning }}</strong> | ||||||
|  |         </div> | ||||||
|  |         <div> | ||||||
|  |           Example: | ||||||
|  |           <strong><code>{{ example }}</code></strong> | ||||||
|  |         </div> | ||||||
|  |         <div> | ||||||
|  |           Equivalent: <strong>{{ equivalent }}</strong> | ||||||
|  |         </div> | ||||||
|  |       </c-card> | ||||||
|  |     </div> | ||||||
|  |     <n-table v-else size="small"> | ||||||
|  |       <thead> | ||||||
|  |         <tr> | ||||||
|  |           <th class="text-left" scope="col"> | ||||||
|  |             Symbol | ||||||
|  |           </th> | ||||||
|  |           <th class="text-left" scope="col"> | ||||||
|  |             Meaning | ||||||
|  |           </th> | ||||||
|  |           <th class="text-left" scope="col"> | ||||||
|  |             Example | ||||||
|  |           </th> | ||||||
|  |           <th class="text-left" scope="col"> | ||||||
|  |             Equivalent | ||||||
|  |           </th> | ||||||
|  |         </tr> | ||||||
|  |       </thead> | ||||||
|  |       <tbody> | ||||||
|  |         <tr v-for="{ symbol, meaning, example, equivalent } in helpers" :key="symbol"> | ||||||
|  |           <td>{{ symbol }}</td> | ||||||
|  |           <td>{{ meaning }}</td> | ||||||
|  |           <td> | ||||||
|  |             <code>{{ example }}</code> | ||||||
|  |           </td> | ||||||
|  |           <td>{{ equivalent }}</td> | ||||||
|  |         </tr> | ||||||
|  |       </tbody> | ||||||
|  |     </n-table> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| ::v-deep(input) { | ::v-deep(input) { | ||||||
|   font-size: 30px; |   font-size: 30px; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { test, expect } from '@playwright/test'; | import { expect, test } from '@playwright/test'; | ||||||
| 
 | 
 | ||||||
| test.describe('Date time converter - json to yaml', () => { | test.describe('Date time converter - json to yaml', () => { | ||||||
|   test.beforeEach(async ({ page }) => { |   test.beforeEach(async ({ page }) => { | ||||||
|  | |||||||
| @ -1,13 +1,13 @@ | |||||||
| import { describe, test, expect } from 'vitest'; | import { describe, expect, test } from 'vitest'; | ||||||
| import { | import { | ||||||
|   isISO8601DateTimeString, |   isISO8601DateTimeString, | ||||||
|   isISO9075DateString, |   isISO9075DateString, | ||||||
|  |   isMongoObjectId, | ||||||
|   isRFC3339DateString, |   isRFC3339DateString, | ||||||
|   isRFC7231DateString, |   isRFC7231DateString, | ||||||
|   isUnixTimestamp, |  | ||||||
|   isTimestamp, |   isTimestamp, | ||||||
|   isUTCDateString, |   isUTCDateString, | ||||||
|   isMongoObjectId, |   isUnixTimestamp, | ||||||
| } from './date-time-converter.models'; | } from './date-time-converter.models'; | ||||||
| 
 | 
 | ||||||
| describe('date-time-converter models', () => { | describe('date-time-converter models', () => { | ||||||
|  | |||||||
| @ -11,13 +11,13 @@ export { | |||||||
|   isMongoObjectId, |   isMongoObjectId, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const ISO8601_REGEX = | const ISO8601_REGEX | ||||||
|   /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; |   = /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; | ||||||
| const ISO9075_REGEX = | const ISO9075_REGEX | ||||||
|   /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/; |   = /^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,6})?(([+-])([0-9]{2}):([0-9]{2})|Z)?$/; | ||||||
| 
 | 
 | ||||||
| const RFC3339_REGEX = | const RFC3339_REGEX | ||||||
|   /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/; |   = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(\.[0-9]{1,9})?(([+-])([0-9]{2}):([0-9]{2})|Z)$/; | ||||||
| 
 | 
 | ||||||
| const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/; | const RFC7231_REGEX = /^[A-Za-z]{3},\s[0-9]{2}\s[A-Za-z]{3}\s[0-9]{4}\s[0-9]{2}:[0-9]{2}:[0-9]{2}\sGMT$/; | ||||||
| 
 | 
 | ||||||
| @ -40,7 +40,8 @@ function isUTCDateString(date?: string) { | |||||||
| 
 | 
 | ||||||
|   try { |   try { | ||||||
|     return new Date(date).toUTCString() === date; |     return new Date(date).toUTCString() === date; | ||||||
|   } catch (_ignored) { |   } | ||||||
|  |   catch (_ignored) { | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
| export type ToDateMapper = (value: string) => Date; | export type ToDateMapper = (value: string) => Date; | ||||||
| 
 | 
 | ||||||
| export type DateFormat = { | export interface DateFormat { | ||||||
|   name: string; |   name: string | ||||||
|   fromDate: (date: Date) => string; |   fromDate: (date: Date) => string | ||||||
|   toDate: (value: string) => Date; |   toDate: (value: string) => Date | ||||||
|   formatMatcher: (dateString: string) => boolean; |   formatMatcher: (dateString: string) => boolean | ||||||
| }; | } | ||||||
|  | |||||||
| @ -1,3 +1,145 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { | ||||||
|  |   formatISO, | ||||||
|  |   formatISO9075, | ||||||
|  |   formatRFC3339, | ||||||
|  |   formatRFC7231, | ||||||
|  |   fromUnixTime, | ||||||
|  |   getTime, | ||||||
|  |   getUnixTime, | ||||||
|  |   isDate, | ||||||
|  |   isValid, | ||||||
|  |   parseISO, | ||||||
|  |   parseJSON, | ||||||
|  | } from 'date-fns'; | ||||||
|  | import type { DateFormat, ToDateMapper } from './date-time-converter.types'; | ||||||
|  | import { | ||||||
|  |   isISO8601DateTimeString, | ||||||
|  |   isISO9075DateString, | ||||||
|  |   isMongoObjectId, | ||||||
|  |   isRFC3339DateString, | ||||||
|  |   isRFC7231DateString, | ||||||
|  |   isTimestamp, | ||||||
|  |   isUTCDateString, | ||||||
|  |   isUnixTimestamp, | ||||||
|  | } from './date-time-converter.models'; | ||||||
|  | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
|  | import { useValidation } from '@/composable/validation'; | ||||||
|  | 
 | ||||||
|  | const inputDate = ref(''); | ||||||
|  | 
 | ||||||
|  | const toDate: ToDateMapper = date => new Date(date); | ||||||
|  | 
 | ||||||
|  | const formats: DateFormat[] = [ | ||||||
|  |   { | ||||||
|  |     name: 'JS locale date string', | ||||||
|  |     fromDate: date => date.toString(), | ||||||
|  |     toDate, | ||||||
|  |     formatMatcher: () => false, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'ISO 8601', | ||||||
|  |     fromDate: formatISO, | ||||||
|  |     toDate: parseISO, | ||||||
|  |     formatMatcher: date => isISO8601DateTimeString(date), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'ISO 9075', | ||||||
|  |     fromDate: formatISO9075, | ||||||
|  |     toDate: parseISO, | ||||||
|  |     formatMatcher: date => isISO9075DateString(date), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'RFC 3339', | ||||||
|  |     fromDate: formatRFC3339, | ||||||
|  |     toDate, | ||||||
|  |     formatMatcher: date => isRFC3339DateString(date), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'RFC 7231', | ||||||
|  |     fromDate: formatRFC7231, | ||||||
|  |     toDate, | ||||||
|  |     formatMatcher: date => isRFC7231DateString(date), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'Unix timestamp', | ||||||
|  |     fromDate: date => String(getUnixTime(date)), | ||||||
|  |     toDate: sec => fromUnixTime(+sec), | ||||||
|  |     formatMatcher: date => isUnixTimestamp(date), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'Timestamp', | ||||||
|  |     fromDate: date => String(getTime(date)), | ||||||
|  |     toDate: ms => parseJSON(+ms), | ||||||
|  |     formatMatcher: date => isTimestamp(date), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'UTC format', | ||||||
|  |     fromDate: date => date.toUTCString(), | ||||||
|  |     toDate, | ||||||
|  |     formatMatcher: date => isUTCDateString(date), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     name: 'Mongo ObjectID', | ||||||
|  |     fromDate: date => `${Math.floor(date.getTime() / 1000).toString(16)}0000000000000000`, | ||||||
|  |     toDate: objectId => new Date(parseInt(objectId.substring(0, 8), 16) * 1000), | ||||||
|  |     formatMatcher: date => isMongoObjectId(date), | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | const formatIndex = ref(6); | ||||||
|  | const now = useNow(); | ||||||
|  | 
 | ||||||
|  | const normalizedDate = computed(() => { | ||||||
|  |   if (!inputDate.value) { | ||||||
|  |     return now.value; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const { toDate } = formats[formatIndex.value]; | ||||||
|  | 
 | ||||||
|  |   try { | ||||||
|  |     return toDate(inputDate.value); | ||||||
|  |   } | ||||||
|  |   catch (_ignored) { | ||||||
|  |     return undefined; | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function onDateInputChanged(value: string) { | ||||||
|  |   const matchingIndex = formats.findIndex(({ formatMatcher }) => formatMatcher(value)); | ||||||
|  |   if (matchingIndex !== -1) { | ||||||
|  |     formatIndex.value = matchingIndex; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const validation = useValidation({ | ||||||
|  |   source: inputDate, | ||||||
|  |   watch: [formatIndex], | ||||||
|  |   rules: [ | ||||||
|  |     { | ||||||
|  |       message: 'This date is invalid for this format', | ||||||
|  |       validator: value => | ||||||
|  |         withDefaultOnError(() => { | ||||||
|  |           if (value === '') { | ||||||
|  |             return true; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           const maybeDate = formats[formatIndex.value].toDate(value); | ||||||
|  |           return isDate(maybeDate) && isValid(maybeDate); | ||||||
|  |         }, false), | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) { | ||||||
|  |   if (!date || !validation.isValid) { | ||||||
|  |     return ''; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return withDefaultOnError(() => formatter(date), ''); | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <n-input-group> |     <n-input-group> | ||||||
| @ -36,142 +178,3 @@ | |||||||
|     /> |     /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { |  | ||||||
|   formatISO, |  | ||||||
|   formatISO9075, |  | ||||||
|   formatRFC3339, |  | ||||||
|   formatRFC7231, |  | ||||||
|   fromUnixTime, |  | ||||||
|   getTime, |  | ||||||
|   getUnixTime, |  | ||||||
|   parseISO, |  | ||||||
|   parseJSON, |  | ||||||
|   isDate, |  | ||||||
|   isValid, |  | ||||||
| } from 'date-fns'; |  | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; |  | ||||||
| import { useValidation } from '@/composable/validation'; |  | ||||||
| import type { DateFormat, ToDateMapper } from './date-time-converter.types'; |  | ||||||
| import { |  | ||||||
|   isISO8601DateTimeString, |  | ||||||
|   isISO9075DateString, |  | ||||||
|   isRFC3339DateString, |  | ||||||
|   isRFC7231DateString, |  | ||||||
|   isTimestamp, |  | ||||||
|   isUTCDateString, |  | ||||||
|   isUnixTimestamp, |  | ||||||
|   isMongoObjectId, |  | ||||||
| } from './date-time-converter.models'; |  | ||||||
| 
 |  | ||||||
| const inputDate = ref(''); |  | ||||||
| 
 |  | ||||||
| const toDate: ToDateMapper = (date) => new Date(date); |  | ||||||
| 
 |  | ||||||
| function formatDateUsingFormatter(formatter: (date: Date) => string, date?: Date) { |  | ||||||
|   if (!date || !validation.isValid) { |  | ||||||
|     return ''; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return withDefaultOnError(() => formatter(date), ''); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const formats: DateFormat[] = [ |  | ||||||
|   { |  | ||||||
|     name: 'JS locale date string', |  | ||||||
|     fromDate: (date) => date.toString(), |  | ||||||
|     toDate, |  | ||||||
|     formatMatcher: () => false, |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     name: 'ISO 8601', |  | ||||||
|     fromDate: formatISO, |  | ||||||
|     toDate: parseISO, |  | ||||||
|     formatMatcher: (date) => isISO8601DateTimeString(date), |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     name: 'ISO 9075', |  | ||||||
|     fromDate: formatISO9075, |  | ||||||
|     toDate: parseISO, |  | ||||||
|     formatMatcher: (date) => isISO9075DateString(date), |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     name: 'RFC 3339', |  | ||||||
|     fromDate: formatRFC3339, |  | ||||||
|     toDate, |  | ||||||
|     formatMatcher: (date) => isRFC3339DateString(date), |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     name: 'RFC 7231', |  | ||||||
|     fromDate: formatRFC7231, |  | ||||||
|     toDate, |  | ||||||
|     formatMatcher: (date) => isRFC7231DateString(date), |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     name: 'Unix timestamp', |  | ||||||
|     fromDate: (date) => String(getUnixTime(date)), |  | ||||||
|     toDate: (sec) => fromUnixTime(+sec), |  | ||||||
|     formatMatcher: (date) => isUnixTimestamp(date), |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     name: 'Timestamp', |  | ||||||
|     fromDate: (date) => String(getTime(date)), |  | ||||||
|     toDate: (ms) => parseJSON(+ms), |  | ||||||
|     formatMatcher: (date) => isTimestamp(date), |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     name: 'UTC format', |  | ||||||
|     fromDate: (date) => date.toUTCString(), |  | ||||||
|     toDate, |  | ||||||
|     formatMatcher: (date) => isUTCDateString(date), |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     name: 'Mongo ObjectID', |  | ||||||
|     fromDate: (date) => Math.floor(date.getTime() / 1000).toString(16) + '0000000000000000', |  | ||||||
|     toDate: (objectId) => new Date(parseInt(objectId.substring(0, 8), 16) * 1000), |  | ||||||
|     formatMatcher: (date) => isMongoObjectId(date), |  | ||||||
|   }, |  | ||||||
| ]; |  | ||||||
| 
 |  | ||||||
| const formatIndex = ref(6); |  | ||||||
| const now = useNow(); |  | ||||||
| 
 |  | ||||||
| const normalizedDate = computed(() => { |  | ||||||
|   if (!inputDate.value) { |  | ||||||
|     return now.value; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const { toDate } = formats[formatIndex.value]; |  | ||||||
| 
 |  | ||||||
|   try { |  | ||||||
|     return toDate(inputDate.value); |  | ||||||
|   } catch (_ignored) { |  | ||||||
|     return undefined; |  | ||||||
|   } |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| function onDateInputChanged(value: string) { |  | ||||||
|   const matchingIndex = formats.findIndex(({ formatMatcher }) => formatMatcher(value)); |  | ||||||
|   if (matchingIndex !== -1) { |  | ||||||
|     formatIndex.value = matchingIndex; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const validation = useValidation({ |  | ||||||
|   source: inputDate, |  | ||||||
|   watch: [formatIndex], |  | ||||||
|   rules: [ |  | ||||||
|     { |  | ||||||
|       message: 'This date is invalid for this format', |  | ||||||
|       validator: (value) => |  | ||||||
|         withDefaultOnError(() => { |  | ||||||
|           if (value === '') return true; |  | ||||||
| 
 |  | ||||||
|           const maybeDate = formats[formatIndex.value].toDate(value); |  | ||||||
|           return isDate(maybeDate) && isValid(maybeDate); |  | ||||||
|         }, false), |  | ||||||
|     }, |  | ||||||
|   ], |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  | |||||||
| @ -1,22 +1,3 @@ | |||||||
| <template> |  | ||||||
|   <c-card v-for="{ name, information } in sections" :key="name" :title="name"> |  | ||||||
|     <n-grid cols="1 400:2" x-gap="12" y-gap="12"> |  | ||||||
|       <n-gi v-for="{ label, value: { value } } in information" :key="label" class="information"> |  | ||||||
|         <div class="label"> |  | ||||||
|           {{ label }} |  | ||||||
|         </div> |  | ||||||
| 
 |  | ||||||
|         <div class="value"> |  | ||||||
|           <n-ellipsis v-if="value"> |  | ||||||
|             {{ value }} |  | ||||||
|           </n-ellipsis> |  | ||||||
|           <div v-else class="undefined-value">unknown</div> |  | ||||||
|         </div> |  | ||||||
|       </n-gi> |  | ||||||
|     </n-grid> |  | ||||||
|   </c-card> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useWindowSize } from '@vueuse/core'; | import { useWindowSize } from '@vueuse/core'; | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| @ -77,6 +58,27 @@ const sections = [ | |||||||
| ]; | ]; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | <template> | ||||||
|  |   <c-card v-for="{ name, information } in sections" :key="name" :title="name"> | ||||||
|  |     <n-grid cols="1 400:2" x-gap="12" y-gap="12"> | ||||||
|  |       <n-gi v-for="{ label, value: { value } } in information" :key="label" class="information"> | ||||||
|  |         <div class="label"> | ||||||
|  |           {{ label }} | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <div class="value"> | ||||||
|  |           <n-ellipsis v-if="value"> | ||||||
|  |             {{ value }} | ||||||
|  |           </n-ellipsis> | ||||||
|  |           <div v-else class="undefined-value"> | ||||||
|  |             unknown | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </n-gi> | ||||||
|  |     </n-grid> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .information { | .information { | ||||||
|   padding: 14px 16px; |   padding: 14px 16px; | ||||||
|  | |||||||
| @ -1,3 +1,34 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import { MessageType, composerize } from 'composerize-ts'; | ||||||
|  | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
|  | import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; | ||||||
|  | import { textToBase64 } from '@/utils/base64'; | ||||||
|  | import TextareaCopyable from '@/components/TextareaCopyable.vue'; | ||||||
|  | 
 | ||||||
|  | const dockerRun = ref( | ||||||
|  |   'docker run -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro --restart always --log-opt max-size=1g nginx', | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const conversionResult = computed(() => | ||||||
|  |   withDefaultOnError(() => composerize(dockerRun.value), { yaml: '', messages: [] }), | ||||||
|  | ); | ||||||
|  | const dockerCompose = computed(() => conversionResult.value.yaml); | ||||||
|  | const notImplemented = computed(() => | ||||||
|  |   conversionResult.value.messages.filter(msg => msg.type === MessageType.notImplemented).map(msg => msg.value), | ||||||
|  | ); | ||||||
|  | const notComposable = computed(() => | ||||||
|  |   conversionResult.value.messages.filter(msg => msg.type === MessageType.notTranslatable).map(msg => msg.value), | ||||||
|  | ); | ||||||
|  | const errors = computed(() => | ||||||
|  |   conversionResult.value.messages | ||||||
|  |     .filter(msg => msg.type === MessageType.errorDuringConversion) | ||||||
|  |     .map(msg => msg.value), | ||||||
|  | ); | ||||||
|  | const dockerComposeBase64 = computed(() => `data:application/yaml;base64,${textToBase64(dockerCompose.value)}`); | ||||||
|  | const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <n-form-item label="Your docker run command:" :show-feedback="false"> |     <n-form-item label="Your docker run command:" :show-feedback="false"> | ||||||
| @ -12,16 +43,20 @@ | |||||||
| 
 | 
 | ||||||
|     <n-divider /> |     <n-divider /> | ||||||
| 
 | 
 | ||||||
|     <textarea-copyable :value="dockerCompose" language="yaml" /> |     <TextareaCopyable :value="dockerCompose" language="yaml" /> | ||||||
| 
 | 
 | ||||||
|     <div mt-5 flex justify-center> |     <div mt-5 flex justify-center> | ||||||
|       <c-button :disabled="dockerCompose === ''" secondary @click="download"> Download docker-compose.yml </c-button> |       <c-button :disabled="dockerCompose === ''" secondary @click="download"> | ||||||
|  |         Download docker-compose.yml | ||||||
|  |       </c-button> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <div v-if="notComposable.length > 0"> |     <div v-if="notComposable.length > 0"> | ||||||
|       <n-alert title="This options are not translatable to docker-compose" type="info" mt-5> |       <n-alert title="This options are not translatable to docker-compose" type="info" mt-5> | ||||||
|         <ul> |         <ul> | ||||||
|           <li v-for="(message, index) of notComposable" :key="index">{{ message }}</li> |           <li v-for="(message, index) of notComposable" :key="index"> | ||||||
|  |             {{ message }} | ||||||
|  |           </li> | ||||||
|         </ul> |         </ul> | ||||||
|       </n-alert> |       </n-alert> | ||||||
|     </div> |     </div> | ||||||
| @ -33,7 +68,9 @@ | |||||||
|         mt-5 |         mt-5 | ||||||
|       > |       > | ||||||
|         <ul> |         <ul> | ||||||
|           <li v-for="(message, index) of notImplemented" :key="index">{{ message }}</li> |           <li v-for="(message, index) of notImplemented" :key="index"> | ||||||
|  |             {{ message }} | ||||||
|  |           </li> | ||||||
|         </ul> |         </ul> | ||||||
|       </n-alert> |       </n-alert> | ||||||
|     </div> |     </div> | ||||||
| @ -41,41 +78,11 @@ | |||||||
|     <div v-if="errors.length > 0"> |     <div v-if="errors.length > 0"> | ||||||
|       <n-alert title="The following errors occured" type="error" mt-5> |       <n-alert title="The following errors occured" type="error" mt-5> | ||||||
|         <ul> |         <ul> | ||||||
|           <li v-for="(message, index) of errors" :key="index">{{ message }}</li> |           <li v-for="(message, index) of errors" :key="index"> | ||||||
|  |             {{ message }} | ||||||
|  |           </li> | ||||||
|         </ul> |         </ul> | ||||||
|       </n-alert> |       </n-alert> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; |  | ||||||
| import { useDownloadFileFromBase64 } from '@/composable/downloadBase64'; |  | ||||||
| import { textToBase64 } from '@/utils/base64'; |  | ||||||
| import TextareaCopyable from '@/components/TextareaCopyable.vue'; |  | ||||||
| 
 |  | ||||||
| import { composerize, MessageType } from 'composerize-ts'; |  | ||||||
| 
 |  | ||||||
| const dockerRun = ref( |  | ||||||
|   'docker run -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro --restart always --log-opt max-size=1g nginx', |  | ||||||
| ); |  | ||||||
| 
 |  | ||||||
| const conversionResult = computed(() => |  | ||||||
|   withDefaultOnError(() => composerize(dockerRun.value), { yaml: '', messages: [] }), |  | ||||||
| ); |  | ||||||
| const dockerCompose = computed(() => conversionResult.value.yaml); |  | ||||||
| const notImplemented = computed(() => |  | ||||||
|   conversionResult.value.messages.filter((msg) => msg.type === MessageType.notImplemented).map((msg) => msg.value), |  | ||||||
| ); |  | ||||||
| const notComposable = computed(() => |  | ||||||
|   conversionResult.value.messages.filter((msg) => msg.type === MessageType.notTranslatable).map((msg) => msg.value), |  | ||||||
| ); |  | ||||||
| const errors = computed(() => |  | ||||||
|   conversionResult.value.messages |  | ||||||
|     .filter((msg) => msg.type === MessageType.errorDuringConversion) |  | ||||||
|     .map((msg) => msg.value), |  | ||||||
| ); |  | ||||||
| const dockerComposeBase64 = computed(() => 'data:application/yaml;base64,' + textToBase64(dockerCompose.value)); |  | ||||||
| const { download } = useDownloadFileFromBase64({ source: dockerComposeBase64, filename: 'docker-compose.yml' }); |  | ||||||
| </script> |  | ||||||
|  | |||||||
| @ -1,3 +1,22 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import { AES, RC4, Rabbit, TripleDES, enc } from 'crypto-js'; | ||||||
|  | 
 | ||||||
|  | const algos = { AES, TripleDES, Rabbit, RC4 }; | ||||||
|  | 
 | ||||||
|  | const cypherInput = ref('Lorem ipsum dolor sit amet'); | ||||||
|  | const cypherAlgo = ref<keyof typeof algos>('AES'); | ||||||
|  | const cypherSecret = ref('my secret key'); | ||||||
|  | const cypherOutput = computed(() => algos[cypherAlgo.value].encrypt(cypherInput.value, cypherSecret.value).toString()); | ||||||
|  | 
 | ||||||
|  | const decryptInput = ref('U2FsdGVkX1/EC3+6P5dbbkZ3e1kQ5o2yzuU0NHTjmrKnLBEwreV489Kr0DIB+uBs'); | ||||||
|  | const decryptAlgo = ref<keyof typeof algos>('AES'); | ||||||
|  | const decryptSecret = ref('my secret key'); | ||||||
|  | const decryptOutput = computed(() => | ||||||
|  |   algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8), | ||||||
|  | ); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <c-card title="Encrypt"> |   <c-card title="Encrypt"> | ||||||
|     <div flex gap-3> |     <div flex gap-3> | ||||||
| @ -78,22 +97,3 @@ | |||||||
|     </n-form-item> |     </n-form-item> | ||||||
|   </c-card> |   </c-card> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| import { AES, TripleDES, Rabbit, RC4, enc } from 'crypto-js'; |  | ||||||
| 
 |  | ||||||
| const algos = { AES, TripleDES, Rabbit, RC4 }; |  | ||||||
| 
 |  | ||||||
| const cypherInput = ref('Lorem ipsum dolor sit amet'); |  | ||||||
| const cypherAlgo = ref<keyof typeof algos>('AES'); |  | ||||||
| const cypherSecret = ref('my secret key'); |  | ||||||
| const cypherOutput = computed(() => algos[cypherAlgo.value].encrypt(cypherInput.value, cypherSecret.value).toString()); |  | ||||||
| 
 |  | ||||||
| const decryptInput = ref('U2FsdGVkX1/EC3+6P5dbbkZ3e1kQ5o2yzuU0NHTjmrKnLBEwreV489Kr0DIB+uBs'); |  | ||||||
| const decryptAlgo = ref<keyof typeof algos>('AES'); |  | ||||||
| const decryptSecret = ref('my secret key'); |  | ||||||
| const decryptOutput = computed(() => |  | ||||||
|   algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8), |  | ||||||
| ); |  | ||||||
| </script> |  | ||||||
|  | |||||||
| @ -1,3 +1,28 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | // Duplicate issue with sub directory | ||||||
|  | 
 | ||||||
|  | import { addMilliseconds, formatRelative } from 'date-fns'; | ||||||
|  | 
 | ||||||
|  | import { enGB } from 'date-fns/locale'; | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import { formatMsDuration } from './eta-calculator.service'; | ||||||
|  | 
 | ||||||
|  | const unitCount = ref(3 * 62); | ||||||
|  | const unitPerTimeSpan = ref(3); | ||||||
|  | const timeSpan = ref(5); | ||||||
|  | const timeSpanUnitMultiplier = ref(60000); | ||||||
|  | const startedAt = ref(Date.now()); | ||||||
|  | 
 | ||||||
|  | const durationMs = computed(() => { | ||||||
|  |   const timeSpanMs = timeSpan.value * timeSpanUnitMultiplier.value; | ||||||
|  | 
 | ||||||
|  |   return unitCount.value / (unitPerTimeSpan.value / timeSpanMs); | ||||||
|  | }); | ||||||
|  | const endAt = computed(() => | ||||||
|  |   formatRelative(addMilliseconds(startedAt.value, durationMs.value), Date.now(), { locale: enGB }), | ||||||
|  | ); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <n-text depth="3" style="text-align: justify; width: 100%; display: inline-block"> |     <n-text depth="3" style="text-align: justify; width: 100%; display: inline-block"> | ||||||
| @ -29,45 +54,24 @@ | |||||||
|             { label: 'hours', value: 1000 * 60 * 60 }, |             { label: 'hours', value: 1000 * 60 * 60 }, | ||||||
|             { label: 'days', value: 1000 * 60 * 60 * 24 }, |             { label: 'days', value: 1000 * 60 * 60 * 24 }, | ||||||
|           ]" |           ]" | ||||||
|         ></n-select> |         /> | ||||||
|       </n-input-group> |       </n-input-group> | ||||||
|     </n-form-item> |     </n-form-item> | ||||||
| 
 | 
 | ||||||
|     <n-divider /> |     <n-divider /> | ||||||
|     <c-card mb-2> |     <c-card mb-2> | ||||||
|       <n-statistic label="Total duration">{{ formatMsDuration(durationMs) }}</n-statistic> |       <n-statistic label="Total duration"> | ||||||
|  |         {{ formatMsDuration(durationMs) }} | ||||||
|  |       </n-statistic> | ||||||
|     </c-card> |     </c-card> | ||||||
|     <c-card> |     <c-card> | ||||||
|       <n-statistic label="It will end ">{{ endAt }}</n-statistic> |       <n-statistic label="It will end "> | ||||||
|  |         {{ endAt }} | ||||||
|  |       </n-statistic> | ||||||
|     </c-card> |     </c-card> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| // Duplicate issue with sub directory |  | ||||||
| // eslint-disable-next-line import/no-duplicates |  | ||||||
| import { addMilliseconds, formatRelative } from 'date-fns'; |  | ||||||
| // eslint-disable-next-line import/no-duplicates |  | ||||||
| import { enGB } from 'date-fns/locale'; |  | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| import { formatMsDuration } from './eta-calculator.service'; |  | ||||||
| 
 |  | ||||||
| const unitCount = ref(3 * 62); |  | ||||||
| const unitPerTimeSpan = ref(3); |  | ||||||
| const timeSpan = ref(5); |  | ||||||
| const timeSpanUnitMultiplier = ref(60000); |  | ||||||
| const startedAt = ref(Date.now()); |  | ||||||
| 
 |  | ||||||
| const durationMs = computed(() => { |  | ||||||
|   const timeSpanMs = timeSpan.value * timeSpanUnitMultiplier.value; |  | ||||||
| 
 |  | ||||||
|   return unitCount.value / (unitPerTimeSpan.value / timeSpanMs); |  | ||||||
| }); |  | ||||||
| const endAt = computed(() => |  | ||||||
|   formatRelative(addMilliseconds(startedAt.value, durationMs.value), Date.now(), { locale: enGB }), |  | ||||||
| ); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .n-input-number, | .n-input-number, | ||||||
| .n-date-picker { | .n-date-picker { | ||||||
|  | |||||||
| @ -1,9 +1,3 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <memo /> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useThemeVars } from 'naive-ui'; | import { useThemeVars } from 'naive-ui'; | ||||||
| import Memo from './git-memo.md'; | import Memo from './git-memo.md'; | ||||||
| @ -11,6 +5,12 @@ import Memo from './git-memo.md'; | |||||||
| const themeVars = useThemeVars(); | const themeVars = useThemeVars(); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <Memo /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| ::v-deep(pre) { | ::v-deep(pre) { | ||||||
|   margin: 0; |   margin: 0; | ||||||
|  | |||||||
| @ -2,6 +2,6 @@ export function convertHexToBin(hex: string) { | |||||||
|   return hex |   return hex | ||||||
|     .trim() |     .trim() | ||||||
|     .split('') |     .split('') | ||||||
|     .map((byte) => parseInt(byte, 16).toString(2).padStart(4, '0')) |     .map(byte => parseInt(byte, 16).toString(2).padStart(4, '0')) | ||||||
|     .join(''); |     .join(''); | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,3 +1,39 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import type { lib } from 'crypto-js'; | ||||||
|  | import { MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512, enc } from 'crypto-js'; | ||||||
|  | import { ref } from 'vue'; | ||||||
|  | import InputCopyable from '../../components/InputCopyable.vue'; | ||||||
|  | import { convertHexToBin } from './hash-text.service'; | ||||||
|  | import { useQueryParam } from '@/composable/queryParams'; | ||||||
|  | 
 | ||||||
|  | const algos = { | ||||||
|  |   MD5, | ||||||
|  |   SHA1, | ||||||
|  |   SHA256, | ||||||
|  |   SHA224, | ||||||
|  |   SHA512, | ||||||
|  |   SHA384, | ||||||
|  |   SHA3, | ||||||
|  |   RIPEMD160, | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | type AlgoNames = keyof typeof algos; | ||||||
|  | type Encoding = keyof typeof enc | 'Bin'; | ||||||
|  | const algoNames = Object.keys(algos) as AlgoNames[]; | ||||||
|  | const encoding = useQueryParam<Encoding>({ defaultValue: 'Hex', name: 'encoding' }); | ||||||
|  | const clearText = ref(''); | ||||||
|  | 
 | ||||||
|  | function formatWithEncoding(words: lib.WordArray, encoding: Encoding) { | ||||||
|  |   if (encoding === 'Bin') { | ||||||
|  |     return convertHexToBin(words.toString(enc.Hex)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return words.toString(enc[encoding]); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[algo](value), encoding.value); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <c-card> |     <c-card> | ||||||
| @ -31,45 +67,12 @@ | |||||||
| 
 | 
 | ||||||
|       <div v-for="algo in algoNames" :key="algo" style="margin: 5px 0"> |       <div v-for="algo in algoNames" :key="algo" style="margin: 5px 0"> | ||||||
|         <n-input-group> |         <n-input-group> | ||||||
|           <n-input-group-label style="flex: 0 0 120px"> {{ algo }} </n-input-group-label> |           <n-input-group-label style="flex: 0 0 120px"> | ||||||
|           <input-copyable :value="hashText(algo, clearText)" readonly /> |             {{ algo }} | ||||||
|  |           </n-input-group-label> | ||||||
|  |           <InputCopyable :value="hashText(algo, clearText)" readonly /> | ||||||
|         </n-input-group> |         </n-input-group> | ||||||
|       </div> |       </div> | ||||||
|     </c-card> |     </c-card> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useQueryParam } from '@/composable/queryParams'; |  | ||||||
| import { enc, lib, MD5, RIPEMD160, SHA1, SHA224, SHA256, SHA3, SHA384, SHA512 } from 'crypto-js'; |  | ||||||
| import { ref } from 'vue'; |  | ||||||
| import InputCopyable from '../../components/InputCopyable.vue'; |  | ||||||
| import { convertHexToBin } from './hash-text.service'; |  | ||||||
| 
 |  | ||||||
| const algos = { |  | ||||||
|   MD5, |  | ||||||
|   SHA1, |  | ||||||
|   SHA256, |  | ||||||
|   SHA224, |  | ||||||
|   SHA512, |  | ||||||
|   SHA384, |  | ||||||
|   SHA3, |  | ||||||
|   RIPEMD160, |  | ||||||
| } as const; |  | ||||||
| 
 |  | ||||||
| type AlgoNames = keyof typeof algos; |  | ||||||
| type Encoding = keyof typeof enc | 'Bin'; |  | ||||||
| const algoNames = Object.keys(algos) as AlgoNames[]; |  | ||||||
| const encoding = useQueryParam<Encoding>({ defaultValue: 'Hex', name: 'encoding' }); |  | ||||||
| const clearText = ref(''); |  | ||||||
| 
 |  | ||||||
| function formatWithEncoding(words: lib.WordArray, encoding: Encoding) { |  | ||||||
|   if (encoding === 'Bin') { |  | ||||||
|     return convertHexToBin(words.toString(enc.Hex)); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return words.toString(enc[encoding]); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const hashText = (algo: AlgoNames, value: string) => formatWithEncoding(algos[algo](value), encoding.value); |  | ||||||
| </script> |  | ||||||
|  | |||||||
| @ -1,3 +1,50 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import type { lib } from 'crypto-js'; | ||||||
|  | import { | ||||||
|  |   HmacMD5, | ||||||
|  |   HmacRIPEMD160, | ||||||
|  |   HmacSHA1, | ||||||
|  |   HmacSHA224, | ||||||
|  |   HmacSHA256, | ||||||
|  |   HmacSHA3, | ||||||
|  |   HmacSHA384, | ||||||
|  |   HmacSHA512, | ||||||
|  |   enc, | ||||||
|  | } from 'crypto-js'; | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import { convertHexToBin } from '../hash-text/hash-text.service'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  | 
 | ||||||
|  | const algos = { | ||||||
|  |   MD5: HmacMD5, | ||||||
|  |   RIPEMD160: HmacRIPEMD160, | ||||||
|  |   SHA1: HmacSHA1, | ||||||
|  |   SHA3: HmacSHA3, | ||||||
|  |   SHA224: HmacSHA224, | ||||||
|  |   SHA256: HmacSHA256, | ||||||
|  |   SHA384: HmacSHA384, | ||||||
|  |   SHA512: HmacSHA512, | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | type Encoding = keyof typeof enc | 'Bin'; | ||||||
|  | 
 | ||||||
|  | function formatWithEncoding(words: lib.WordArray, encoding: Encoding) { | ||||||
|  |   if (encoding === 'Bin') { | ||||||
|  |     return convertHexToBin(words.toString(enc.Hex)); | ||||||
|  |   } | ||||||
|  |   return words.toString(enc[encoding]); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const plainText = ref(''); | ||||||
|  | const secret = ref(''); | ||||||
|  | const hashFunction = ref<keyof typeof algos>('SHA256'); | ||||||
|  | const encoding = ref<Encoding>('Hex'); | ||||||
|  | const hmac = computed(() => | ||||||
|  |   formatWithEncoding(algos[hashFunction.value](plainText.value, secret.value), encoding.value), | ||||||
|  | ); | ||||||
|  | const { copy } = useCopy({ source: hmac }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <n-form-item label="Plain text to compute the hash"> |     <n-form-item label="Plain text to compute the hash"> | ||||||
| @ -43,54 +90,9 @@ | |||||||
|       <n-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." /> |       <n-input readonly :value="hmac" type="textarea" placeholder="The result of the HMAC..." /> | ||||||
|     </n-form-item> |     </n-form-item> | ||||||
|     <div flex justify-center> |     <div flex justify-center> | ||||||
|       <c-button @click="copy()">Copy HMAC</c-button> |       <c-button @click="copy()"> | ||||||
|  |         Copy HMAC | ||||||
|  |       </c-button> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useCopy } from '@/composable/copy'; |  | ||||||
| import { |  | ||||||
|   enc, |  | ||||||
|   HmacMD5, |  | ||||||
|   HmacRIPEMD160, |  | ||||||
|   HmacSHA1, |  | ||||||
|   HmacSHA224, |  | ||||||
|   HmacSHA256, |  | ||||||
|   HmacSHA3, |  | ||||||
|   HmacSHA384, |  | ||||||
|   HmacSHA512, |  | ||||||
|   lib, |  | ||||||
| } from 'crypto-js'; |  | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| import { convertHexToBin } from '../hash-text/hash-text.service'; |  | ||||||
| 
 |  | ||||||
| const algos = { |  | ||||||
|   MD5: HmacMD5, |  | ||||||
|   RIPEMD160: HmacRIPEMD160, |  | ||||||
|   SHA1: HmacSHA1, |  | ||||||
|   SHA3: HmacSHA3, |  | ||||||
|   SHA224: HmacSHA224, |  | ||||||
|   SHA256: HmacSHA256, |  | ||||||
|   SHA384: HmacSHA384, |  | ||||||
|   SHA512: HmacSHA512, |  | ||||||
| } as const; |  | ||||||
| 
 |  | ||||||
| type Encoding = keyof typeof enc | 'Bin'; |  | ||||||
| 
 |  | ||||||
| function formatWithEncoding(words: lib.WordArray, encoding: Encoding) { |  | ||||||
|   if (encoding === 'Bin') { |  | ||||||
|     return convertHexToBin(words.toString(enc.Hex)); |  | ||||||
|   } |  | ||||||
|   return words.toString(enc[encoding]); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const plainText = ref(''); |  | ||||||
| const secret = ref(''); |  | ||||||
| const hashFunction = ref<keyof typeof algos>('SHA256'); |  | ||||||
| const encoding = ref<Encoding>('Hex'); |  | ||||||
| const hmac = computed(() => |  | ||||||
|   formatWithEncoding(algos[hashFunction.value](plainText.value, secret.value), encoding.value), |  | ||||||
| ); |  | ||||||
| const { copy } = useCopy({ source: hmac }); |  | ||||||
| </script> |  | ||||||
|  | |||||||
| @ -1,3 +1,17 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { escape, unescape } from 'lodash'; | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  | 
 | ||||||
|  | const escapeInput = ref('<title>IT Tool</title>'); | ||||||
|  | const escapeOutput = computed(() => escape(escapeInput.value)); | ||||||
|  | const { copy: copyEscaped } = useCopy({ source: escapeOutput }); | ||||||
|  | 
 | ||||||
|  | const unescapeInput = ref('<title>IT Tool</title'); | ||||||
|  | const unescapeOutput = computed(() => unescape(unescapeInput.value)); | ||||||
|  | const { copy: copyUnescaped } = useCopy({ source: unescapeOutput }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <c-card title="Escape html entities"> |   <c-card title="Escape html entities"> | ||||||
|     <n-form-item label="Your string :"> |     <n-form-item label="Your string :"> | ||||||
| @ -20,7 +34,9 @@ | |||||||
|     </n-form-item> |     </n-form-item> | ||||||
| 
 | 
 | ||||||
|     <div flex justify-center> |     <div flex justify-center> | ||||||
|       <c-button @click="copyEscaped"> Copy </c-button> |       <c-button @click="copyEscaped"> | ||||||
|  |         Copy | ||||||
|  |       </c-button> | ||||||
|     </div> |     </div> | ||||||
|   </c-card> |   </c-card> | ||||||
|   <c-card title="Unescape html entities"> |   <c-card title="Unescape html entities"> | ||||||
| @ -44,21 +60,9 @@ | |||||||
|     </n-form-item> |     </n-form-item> | ||||||
| 
 | 
 | ||||||
|     <div flex justify-center> |     <div flex justify-center> | ||||||
|       <c-button @click="copyUnescaped"> Copy </c-button> |       <c-button @click="copyUnescaped"> | ||||||
|  |         Copy | ||||||
|  |       </c-button> | ||||||
|     </div> |     </div> | ||||||
|   </c-card> |   </c-card> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { escape, unescape } from 'lodash'; |  | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| import { useCopy } from '@/composable/copy'; |  | ||||||
| 
 |  | ||||||
| const escapeInput = ref('<title>IT Tool</title>'); |  | ||||||
| const escapeOutput = computed(() => escape(escapeInput.value)); |  | ||||||
| const { copy: copyEscaped } = useCopy({ source: escapeOutput }); |  | ||||||
| 
 |  | ||||||
| const unescapeInput = ref('<title>IT Tool</title'); |  | ||||||
| const unescapeOutput = computed(() => unescape(unescapeInput.value)); |  | ||||||
| const { copy: copyUnescaped } = useCopy({ source: unescapeOutput }); |  | ||||||
| </script> |  | ||||||
|  | |||||||
| @ -1,14 +1,3 @@ | |||||||
| <template> |  | ||||||
|   <c-card v-if="editor" important:p0> |  | ||||||
|     <menu-bar class="editor-header" :editor="editor" /> |  | ||||||
|     <n-divider style="margin-top: 0" /> |  | ||||||
| 
 |  | ||||||
|     <div px8 pb6> |  | ||||||
|       <editor-content class="editor-content" :editor="editor" /> |  | ||||||
|     </div> |  | ||||||
|   </c-card> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { tryOnBeforeUnmount, useVModel } from '@vueuse/core'; | import { tryOnBeforeUnmount, useVModel } from '@vueuse/core'; | ||||||
| import { Editor, EditorContent } from '@tiptap/vue-3'; | import { Editor, EditorContent } from '@tiptap/vue-3'; | ||||||
| @ -16,9 +5,9 @@ import StarterKit from '@tiptap/starter-kit'; | |||||||
| import { useThemeVars } from 'naive-ui'; | import { useThemeVars } from 'naive-ui'; | ||||||
| import MenuBar from './menu-bar.vue'; | import MenuBar from './menu-bar.vue'; | ||||||
| 
 | 
 | ||||||
| const themeVars = useThemeVars(); |  | ||||||
| const props = defineProps<{ html: string }>(); | const props = defineProps<{ html: string }>(); | ||||||
| const emit = defineEmits(['update:html']); | const emit = defineEmits(['update:html']); | ||||||
|  | const themeVars = useThemeVars(); | ||||||
| const html = useVModel(props, 'html', emit); | const html = useVModel(props, 'html', emit); | ||||||
| 
 | 
 | ||||||
| const editor = new Editor({ | const editor = new Editor({ | ||||||
| @ -33,6 +22,17 @@ tryOnBeforeUnmount(() => { | |||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | <template> | ||||||
|  |   <c-card v-if="editor" important:p0> | ||||||
|  |     <MenuBar class="editor-header" :editor="editor" /> | ||||||
|  |     <n-divider style="margin-top: 0" /> | ||||||
|  | 
 | ||||||
|  |     <div px8 pb6> | ||||||
|  |       <EditorContent class="editor-content" :editor="editor" /> | ||||||
|  |     </div> | ||||||
|  |   </c-card> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
| <style scoped lang="less"> | <style scoped lang="less"> | ||||||
| ::v-deep(.ProseMirror-focused) { | ::v-deep(.ProseMirror-focused) { | ||||||
|   outline: none; |   outline: none; | ||||||
|  | |||||||
| @ -1,3 +1,10 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { type Component, toRefs } from 'vue'; | ||||||
|  | 
 | ||||||
|  | const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean }>(); | ||||||
|  | const { icon, title, action, isActive } = toRefs(props); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <n-tooltip trigger="hover"> |   <n-tooltip trigger="hover"> | ||||||
|     <template #trigger> |     <template #trigger> | ||||||
| @ -9,12 +16,3 @@ | |||||||
|     {{ title }} |     {{ title }} | ||||||
|   </n-tooltip> |   </n-tooltip> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { toRefs, type Component } from 'vue'; |  | ||||||
| 
 |  | ||||||
| const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean }>(); |  | ||||||
| const { icon, title, action, isActive } = toRefs(props); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style scoped></style> |  | ||||||
|  | |||||||
| @ -1,12 +1,3 @@ | |||||||
| <template> |  | ||||||
|   <div flex items-center> |  | ||||||
|     <template v-for="(item, index) in items"> |  | ||||||
|       <n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical /> |  | ||||||
|       <menu-bar-item v-else-if="item.type === 'button'" :key="index" v-bind="item" /> |  | ||||||
|     </template> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import type { Editor } from '@tiptap/vue-3'; | import type { Editor } from '@tiptap/vue-3'; | ||||||
| import { | import { | ||||||
| @ -27,7 +18,7 @@ import { | |||||||
|   Strikethrough, |   Strikethrough, | ||||||
|   TextWrap, |   TextWrap, | ||||||
| } from '@vicons/tabler'; | } from '@vicons/tabler'; | ||||||
| import { toRefs, type Component } from 'vue'; | import { type Component, toRefs } from 'vue'; | ||||||
| import MenuBarItem from './menu-bar-item.vue'; | import MenuBarItem from './menu-bar-item.vue'; | ||||||
| 
 | 
 | ||||||
| const props = defineProps<{ editor: Editor }>(); | const props = defineProps<{ editor: Editor }>(); | ||||||
| @ -35,12 +26,12 @@ const { editor } = toRefs(props); | |||||||
| 
 | 
 | ||||||
| type MenuItem = | type MenuItem = | ||||||
|   | { |   | { | ||||||
|       icon: Component; |     icon: Component | ||||||
|       title: string; |     title: string | ||||||
|       action: () => void; |     action: () => void | ||||||
|       isActive?: () => boolean; |     isActive?: () => boolean | ||||||
|       type: 'button'; |     type: 'button' | ||||||
|     } |   } | ||||||
|   | { type: 'divider' }; |   | { type: 'divider' }; | ||||||
| 
 | 
 | ||||||
| const items: MenuItem[] = [ | const items: MenuItem[] = [ | ||||||
| @ -166,4 +157,11 @@ const items: MenuItem[] = [ | |||||||
| ]; | ]; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped></style> | <template> | ||||||
|  |   <div flex items-center> | ||||||
|  |     <template v-for="(item, index) in items"> | ||||||
|  |       <n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical /> | ||||||
|  |       <MenuBarItem v-else-if="item.type === 'button'" :key="index" v-bind="item" /> | ||||||
|  |     </template> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,16 +1,14 @@ | |||||||
| <template> |  | ||||||
|   <editor v-model:html="html" /> |  | ||||||
|   <textarea-copyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" /> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import TextareaCopyable from '@/components/TextareaCopyable.vue'; |  | ||||||
| import { format } from 'prettier'; | import { format } from 'prettier'; | ||||||
| import htmlParser from 'prettier/parser-html'; | import htmlParser from 'prettier/parser-html'; | ||||||
| import { useStorage } from '@vueuse/core'; | import { useStorage } from '@vueuse/core'; | ||||||
| import Editor from './editor/editor.vue'; | import Editor from './editor/editor.vue'; | ||||||
|  | import TextareaCopyable from '@/components/TextareaCopyable.vue'; | ||||||
| 
 | 
 | ||||||
| const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>'); | const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>'); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="less" scoped></style> | <template> | ||||||
|  |   <Editor v-model:html="html" /> | ||||||
|  |   <TextareaCopyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" /> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,11 +1,11 @@ | |||||||
| export const codesByCategories: { | export const codesByCategories: { | ||||||
|   category: string; |   category: string | ||||||
|   codes: { |   codes: { | ||||||
|     code: number; |     code: number | ||||||
|     name: string; |     name: string | ||||||
|     description: string; |     description: string | ||||||
|     type: 'HTTP' | 'WebDav'; |     type: 'HTTP' | 'WebDav' | ||||||
|   }[]; |   }[] | ||||||
| }[] = [ | }[] = [ | ||||||
|   { |   { | ||||||
|     category: '1xx informational response', |     category: '1xx informational response', | ||||||
| @ -286,7 +286,7 @@ export const codesByCategories: { | |||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         code: 418, |         code: 418, | ||||||
|         name: "I'm a teapot", |         name: 'I\'m a teapot', | ||||||
|         description: 'The server refuses the attempt to brew coffee with a teapot.', |         description: 'The server refuses the attempt to brew coffee with a teapot.', | ||||||
|         type: 'HTTP', |         type: 'HTTP', | ||||||
|       }, |       }, | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { test, expect } from '@playwright/test'; | import { expect, test } from '@playwright/test'; | ||||||
| 
 | 
 | ||||||
| test.describe('Tool - Http status codes', () => { | test.describe('Tool - Http status codes', () => { | ||||||
|   test.beforeEach(async ({ page }) => { |   test.beforeEach(async ({ page }) => { | ||||||
|  | |||||||
| @ -1,3 +1,27 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { SearchRound } from '@vicons/material'; | ||||||
|  | import { codesByCategories } from './http-status-codes.constants'; | ||||||
|  | import { useFuzzySearch } from '@/composable/fuzzySearch'; | ||||||
|  | 
 | ||||||
|  | const search = ref(''); | ||||||
|  | 
 | ||||||
|  | const { searchResult } = useFuzzySearch({ | ||||||
|  |   search, | ||||||
|  |   data: codesByCategories.flatMap(({ codes, category }) => codes.map(code => ({ ...code, category }))), | ||||||
|  |   options: { | ||||||
|  |     keys: [{ name: 'code', weight: 3 }, { name: 'name', weight: 2 }, 'description', 'category'], | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const codesByCategoryFiltered = computed(() => { | ||||||
|  |   if (!search.value) { | ||||||
|  |     return codesByCategories; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return [{ category: 'Search results', codes: searchResult.value }]; | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <n-form-item :show-label="false"> |     <n-form-item :show-label="false"> | ||||||
| @ -21,35 +45,13 @@ | |||||||
|       <n-h2> {{ category }} </n-h2> |       <n-h2> {{ category }} </n-h2> | ||||||
| 
 | 
 | ||||||
|       <c-card v-for="{ code, description, name, type } of codes" :key="code" mb-2> |       <c-card v-for="{ code, description, name, type } of codes" :key="code" mb-2> | ||||||
|         <n-text strong block text-lg> {{ code }} {{ name }} </n-text> |         <n-text strong block text-lg> | ||||||
|         <n-text block depth="3">{{ description }} {{ type !== 'HTTP' ? `For ${type}.` : '' }}</n-text> |           {{ code }} {{ name }} | ||||||
|  |         </n-text> | ||||||
|  |         <n-text block depth="3"> | ||||||
|  |           {{ description }} {{ type !== 'HTTP' ? `For ${type}.` : '' }} | ||||||
|  |         </n-text> | ||||||
|       </c-card> |       </c-card> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useFuzzySearch } from '@/composable/fuzzySearch'; |  | ||||||
| import { SearchRound } from '@vicons/material'; |  | ||||||
| import { codesByCategories } from './http-status-codes.constants'; |  | ||||||
| 
 |  | ||||||
| const search = ref(''); |  | ||||||
| 
 |  | ||||||
| const { searchResult } = useFuzzySearch({ |  | ||||||
|   search, |  | ||||||
|   data: codesByCategories.flatMap(({ codes, category }) => codes.map((code) => ({ ...code, category }))), |  | ||||||
|   options: { |  | ||||||
|     keys: [{ name: 'code', weight: 3 }, { name: 'name', weight: 2 }, 'description', 'category'], |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| const codesByCategoryFiltered = computed(() => { |  | ||||||
|   if (!search.value) { |  | ||||||
|     return codesByCategories; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return [{ category: 'Search results', codes: searchResult.value }]; |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped></style> |  | ||||||
|  | |||||||
| @ -140,5 +140,5 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
| 
 | 
 | ||||||
| export const tools = toolsByCategory.flatMap(({ components }) => components); | export const tools = toolsByCategory.flatMap(({ components }) => components); | ||||||
| export const toolsWithCategory = toolsByCategory.flatMap(({ components, name }) => | export const toolsWithCategory = toolsByCategory.flatMap(({ components, name }) => | ||||||
|   components.map((tool) => ({ category: name, ...tool })), |   components.map(tool => ({ category: name, ...tool })), | ||||||
| ); | ); | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { expect, describe, it } from 'vitest'; | import { describe, expect, it } from 'vitest'; | ||||||
| import { convertBase } from './integer-base-converter.model'; | import { convertBase } from './integer-base-converter.model'; | ||||||
| 
 | 
 | ||||||
| describe('integer-base-converter', () => { | describe('integer-base-converter', () => { | ||||||
|  | |||||||
| @ -7,9 +7,9 @@ export function convertBase({ value, fromBase, toBase }: { value: string; fromBa | |||||||
|     .reverse() |     .reverse() | ||||||
|     .reduce((carry: number, digit: string, index: number) => { |     .reduce((carry: number, digit: string, index: number) => { | ||||||
|       if (!fromRange.includes(digit)) { |       if (!fromRange.includes(digit)) { | ||||||
|         throw new Error('Invalid digit "' + digit + '" for base ' + fromBase + '.'); |         throw new Error(`Invalid digit "${digit}" for base ${fromBase}.`); | ||||||
|       } |       } | ||||||
|       return (carry += fromRange.indexOf(digit) * Math.pow(fromBase, index)); |       return (carry += fromRange.indexOf(digit) * fromBase ** index); | ||||||
|     }, 0); |     }, 0); | ||||||
|   let newValue = ''; |   let newValue = ''; | ||||||
|   while (decValue > 0) { |   while (decValue > 0) { | ||||||
|  | |||||||
| @ -1,56 +1,103 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import InputCopyable from '../../components/InputCopyable.vue'; | ||||||
|  | import { convertBase } from './integer-base-converter.model'; | ||||||
|  | import { useStyleStore } from '@/stores/style.store'; | ||||||
|  | import { getErrorMessageIfThrows } from '@/utils/error'; | ||||||
|  | 
 | ||||||
|  | const styleStore = useStyleStore(); | ||||||
|  | 
 | ||||||
|  | const inputProps = { | ||||||
|  |   'labelPosition': 'left', | ||||||
|  |   'labelWidth': '170px', | ||||||
|  |   'labelAlign': 'right', | ||||||
|  |   'readonly': true, | ||||||
|  |   'mb-2': '', | ||||||
|  | } as const; | ||||||
|  | 
 | ||||||
|  | const input = ref('42'); | ||||||
|  | const inputBase = ref(10); | ||||||
|  | const outputBase = ref(42); | ||||||
|  | 
 | ||||||
|  | function errorlessConvert(...args: Parameters<typeof convertBase>) { | ||||||
|  |   try { | ||||||
|  |     return convertBase(...args); | ||||||
|  |   } | ||||||
|  |   catch (err) { | ||||||
|  |     return ''; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const error = computed(() => | ||||||
|  |   getErrorMessageIfThrows(() => | ||||||
|  |     convertBase({ value: input.value, fromBase: inputBase.value, toBase: outputBase.value }), | ||||||
|  |   ), | ||||||
|  | ); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <c-card> |     <c-card> | ||||||
|       <div v-if="styleStore.isSmallScreen"> |       <div v-if="styleStore.isSmallScreen"> | ||||||
|         <n-input-group> |         <n-input-group> | ||||||
|           <n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label> |           <n-input-group-label style="flex: 0 0 120px"> | ||||||
|  |             Input number: | ||||||
|  |           </n-input-group-label> | ||||||
|           <n-input v-model:value="input" w-full :status="error ? 'error' : undefined" /> |           <n-input v-model:value="input" w-full :status="error ? 'error' : undefined" /> | ||||||
|         </n-input-group> |         </n-input-group> | ||||||
|         <n-input-group> |         <n-input-group> | ||||||
|           <n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label> |           <n-input-group-label style="flex: 0 0 120px"> | ||||||
|  |             Input base: | ||||||
|  |           </n-input-group-label> | ||||||
|           <n-input-number v-model:value="inputBase" max="64" min="2" w-full /> |           <n-input-number v-model:value="inputBase" max="64" min="2" w-full /> | ||||||
|         </n-input-group> |         </n-input-group> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <n-input-group v-else> |       <n-input-group v-else> | ||||||
|         <n-input-group-label style="flex: 0 0 120px"> Input number: </n-input-group-label> |         <n-input-group-label style="flex: 0 0 120px"> | ||||||
|  |           Input number: | ||||||
|  |         </n-input-group-label> | ||||||
|         <n-input v-model:value="input" :status="error ? 'error' : undefined" /> |         <n-input v-model:value="input" :status="error ? 'error' : undefined" /> | ||||||
|         <n-input-group-label style="flex: 0 0 120px"> Input base: </n-input-group-label> |         <n-input-group-label style="flex: 0 0 120px"> | ||||||
|  |           Input base: | ||||||
|  |         </n-input-group-label> | ||||||
|         <n-input-number v-model:value="inputBase" max="64" min="2" /> |         <n-input-number v-model:value="inputBase" max="64" min="2" /> | ||||||
|       </n-input-group> |       </n-input-group> | ||||||
| 
 | 
 | ||||||
|       <n-alert v-if="error" style="margin-top: 25px" type="error">{{ error }}</n-alert> |       <n-alert v-if="error" style="margin-top: 25px" type="error"> | ||||||
|  |         {{ error }} | ||||||
|  |       </n-alert> | ||||||
|       <n-divider /> |       <n-divider /> | ||||||
| 
 | 
 | ||||||
|       <input-copyable |       <InputCopyable | ||||||
|         label="Binary (2)" |         label="Binary (2)" | ||||||
|         v-bind="inputProps" |         v-bind="inputProps" | ||||||
|         :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })" |         :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 2 })" | ||||||
|         placeholder="Binary version will be here..." |         placeholder="Binary version will be here..." | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|       <input-copyable |       <InputCopyable | ||||||
|         label="Octal (8)" |         label="Octal (8)" | ||||||
|         v-bind="inputProps" |         v-bind="inputProps" | ||||||
|         :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })" |         :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 8 })" | ||||||
|         placeholder="Octal version will be here..." |         placeholder="Octal version will be here..." | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|       <input-copyable |       <InputCopyable | ||||||
|         label="Decimal (10)" |         label="Decimal (10)" | ||||||
|         v-bind="inputProps" |         v-bind="inputProps" | ||||||
|         :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })" |         :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 10 })" | ||||||
|         placeholder="Decimal version will be here..." |         placeholder="Decimal version will be here..." | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|       <input-copyable |       <InputCopyable | ||||||
|         label="Hexadecimal (16)" |         label="Hexadecimal (16)" | ||||||
|         v-bind="inputProps" |         v-bind="inputProps" | ||||||
|         :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })" |         :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 16 })" | ||||||
|         placeholder="Hexadecimal version will be here..." |         placeholder="Hexadecimal version will be here..." | ||||||
|       /> |       /> | ||||||
| 
 | 
 | ||||||
|       <input-copyable |       <InputCopyable | ||||||
|         label="Base64 (64)" |         label="Base64 (64)" | ||||||
|         v-bind="inputProps" |         v-bind="inputProps" | ||||||
|         :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })" |         :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: 64 })" | ||||||
| @ -63,7 +110,7 @@ | |||||||
|           <n-input-number v-model:value="outputBase" max="64" min="2" /> |           <n-input-number v-model:value="outputBase" max="64" min="2" /> | ||||||
|         </n-input-group> |         </n-input-group> | ||||||
| 
 | 
 | ||||||
|         <input-copyable |         <InputCopyable | ||||||
|           flex-1 |           flex-1 | ||||||
|           v-bind="inputProps" |           v-bind="inputProps" | ||||||
|           :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })" |           :value="errorlessConvert({ value: input, fromBase: inputBase, toBase: outputBase })" | ||||||
| @ -74,42 +121,6 @@ | |||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| import { useStyleStore } from '@/stores/style.store'; |  | ||||||
| import { getErrorMessageIfThrows } from '@/utils/error'; |  | ||||||
| import { convertBase } from './integer-base-converter.model'; |  | ||||||
| import InputCopyable from '../../components/InputCopyable.vue'; |  | ||||||
| 
 |  | ||||||
| const styleStore = useStyleStore(); |  | ||||||
| 
 |  | ||||||
| const inputProps = { |  | ||||||
|   labelPosition: 'left', |  | ||||||
|   labelWidth: '170px', |  | ||||||
|   labelAlign: 'right', |  | ||||||
|   readonly: true, |  | ||||||
|   'mb-2': '', |  | ||||||
| } as const; |  | ||||||
| 
 |  | ||||||
| const input = ref('42'); |  | ||||||
| const inputBase = ref(10); |  | ||||||
| const outputBase = ref(42); |  | ||||||
| 
 |  | ||||||
| function errorlessConvert(...args: Parameters<typeof convertBase>) { |  | ||||||
|   try { |  | ||||||
|     return convertBase(...args); |  | ||||||
|   } catch (err) { |  | ||||||
|     return ''; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const error = computed(() => |  | ||||||
|   getErrorMessageIfThrows(() => |  | ||||||
|     convertBase({ value: input.value, fromBase: inputBase.value, toBase: outputBase.value }), |  | ||||||
|   ), |  | ||||||
| ); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .n-input-group:not(:first-child) { | .n-input-group:not(:first-child) { | ||||||
|   margin-top: 5px; |   margin-top: 5px; | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { expect, describe, it } from 'vitest'; | import { describe, expect, it } from 'vitest'; | ||||||
| import { isValidIpv4, ipv4ToInt } from './ipv4-address-converter.service'; | import { ipv4ToInt, isValidIpv4 } from './ipv4-address-converter.service'; | ||||||
| 
 | 
 | ||||||
| describe('ipv4-address-converter', () => { | describe('ipv4-address-converter', () => { | ||||||
|   describe('ipv4ToInt', () => { |   describe('ipv4ToInt', () => { | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ function ipv4ToInt({ ip }: { ip: string }) { | |||||||
|   return ip |   return ip | ||||||
|     .trim() |     .trim() | ||||||
|     .split('.') |     .split('.') | ||||||
|     .reduce((acc, part, index) => acc + Number(part) * Math.pow(256, 3 - index), 0); |     .reduce((acc, part, index) => acc + Number(part) * 256 ** (3 - index), 0); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: string; prefix?: string }) { | function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: string; prefix?: string }) { | ||||||
| @ -19,13 +19,13 @@ function ipv4ToIpv6({ ip, prefix = '0000:0000:0000:0000:0000:ffff:' }: { ip: str | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     prefix + |     prefix | ||||||
|     _.chain(ip) |     + _.chain(ip) | ||||||
|       .trim() |       .trim() | ||||||
|       .split('.') |       .split('.') | ||||||
|       .map((part) => parseInt(part).toString(16).padStart(2, '0')) |       .map(part => parseInt(part).toString(16).padStart(2, '0')) | ||||||
|       .chunk(2) |       .chunk(2) | ||||||
|       .map((blocks) => blocks.join('')) |       .map(blocks => blocks.join('')) | ||||||
|       .join(':') |       .join(':') | ||||||
|       .value() |       .value() | ||||||
|   ); |   ); | ||||||
|  | |||||||
| @ -1,27 +1,7 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." /> |  | ||||||
| 
 |  | ||||||
|     <n-divider /> |  | ||||||
| 
 |  | ||||||
|     <input-copyable |  | ||||||
|       v-for="{ label, value } of convertedSections" |  | ||||||
|       :key="label" |  | ||||||
|       :label="label" |  | ||||||
|       label-position="left" |  | ||||||
|       label-width="100px" |  | ||||||
|       label-align="right" |  | ||||||
|       mb-2 |  | ||||||
|       :value="validationAttrs.validationStatus === 'error' ? '' : value" |  | ||||||
|       placeholder="Set a correct ipv4 address" |  | ||||||
|     /> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { useValidation } from '@/composable/validation'; |  | ||||||
| import { convertBase } from '../integer-base-converter/integer-base-converter.model'; | import { convertBase } from '../integer-base-converter/integer-base-converter.model'; | ||||||
| import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from './ipv4-address-converter.service'; | import { ipv4ToInt, ipv4ToIpv6, isValidIpv4 } from './ipv4-address-converter.service'; | ||||||
|  | import { useValidation } from '@/composable/validation'; | ||||||
| 
 | 
 | ||||||
| const rawIpAddress = useStorage('ipv4-converter:ip', '192.168.1.1'); | const rawIpAddress = useStorage('ipv4-converter:ip', '192.168.1.1'); | ||||||
| 
 | 
 | ||||||
| @ -54,8 +34,26 @@ const convertedSections = computed(() => { | |||||||
| 
 | 
 | ||||||
| const { attrs: validationAttrs } = useValidation({ | const { attrs: validationAttrs } = useValidation({ | ||||||
|   source: rawIpAddress, |   source: rawIpAddress, | ||||||
|   rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }], |   rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }], | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="less" scoped></style> | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-input-text v-model:value="rawIpAddress" label="The ipv4 address:" placeholder="The ipv4 address..." /> | ||||||
|  | 
 | ||||||
|  |     <n-divider /> | ||||||
|  | 
 | ||||||
|  |     <input-copyable | ||||||
|  |       v-for="{ label, value } of convertedSections" | ||||||
|  |       :key="label" | ||||||
|  |       :label="label" | ||||||
|  |       label-position="left" | ||||||
|  |       label-width="100px" | ||||||
|  |       label-align="right" | ||||||
|  |       mb-2 | ||||||
|  |       :value="validationAttrs.validationStatus === 'error' ? '' : value" | ||||||
|  |       placeholder="Set a correct ipv4 address" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { test, expect } from '@playwright/test'; | import { expect, test } from '@playwright/test'; | ||||||
| 
 | 
 | ||||||
| test.describe('Tool - IPv4 range expander', () => { | test.describe('Tool - IPv4 range expander', () => { | ||||||
|   test.beforeEach(async ({ page }) => { |   test.beforeEach(async ({ page }) => { | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { expect, describe, it } from 'vitest'; | import { describe, expect, it } from 'vitest'; | ||||||
| import { calculateCidr } from './ipv4-range-expander.service'; | import { calculateCidr } from './ipv4-range-expander.service'; | ||||||
| 
 | 
 | ||||||
| describe('ipv4RangeExpander', () => { | describe('ipv4RangeExpander', () => { | ||||||
|  | |||||||
| @ -1,20 +1,25 @@ | |||||||
| import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types'; |  | ||||||
| import { convertBase } from '../integer-base-converter/integer-base-converter.model'; | import { convertBase } from '../integer-base-converter/integer-base-converter.model'; | ||||||
| import { ipv4ToInt } from '../ipv4-address-converter/ipv4-address-converter.service'; | import { ipv4ToInt } from '../ipv4-address-converter/ipv4-address-converter.service'; | ||||||
|  | import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types'; | ||||||
|  | 
 | ||||||
| export { calculateCidr }; | export { calculateCidr }; | ||||||
| 
 | 
 | ||||||
| function bits2ip(ipInt: number) { | function bits2ip(ipInt: number) { | ||||||
|   return (ipInt >>> 24) + '.' + ((ipInt >> 16) & 255) + '.' + ((ipInt >> 8) & 255) + '.' + (ipInt & 255); |   return `${ipInt >>> 24}.${(ipInt >> 16) & 255}.${(ipInt >> 8) & 255}.${ipInt & 255}`; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getRangesize(start: string, end: string) { | function getRangesize(start: string, end: string) { | ||||||
|   if (start == null || end == null) return -1; |   if (start == null || end == null) { | ||||||
|  |     return -1; | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   return 1 + parseInt(end, 2) - parseInt(start, 2); |   return 1 + parseInt(end, 2) - parseInt(start, 2); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getCidr(start: string, end: string) { | function getCidr(start: string, end: string) { | ||||||
|   if (start == null || end == null) return null; |   if (start == null || end == null) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   const range = getRangesize(start, end); |   const range = getRangesize(start, end); | ||||||
|   if (range < 1) { |   if (range < 1) { | ||||||
| @ -32,7 +37,7 @@ function getCidr(start: string, end: string) { | |||||||
|   const newStart = start.substring(0, mask) + '0'.repeat(32 - mask); |   const newStart = start.substring(0, mask) + '0'.repeat(32 - mask); | ||||||
|   const newEnd = end.substring(0, mask) + '1'.repeat(32 - mask); |   const newEnd = end.substring(0, mask) + '1'.repeat(32 - mask); | ||||||
| 
 | 
 | ||||||
|   return { start: newStart, end: newEnd, mask: mask }; |   return { start: newStart, end: newEnd, mask }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) { | function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) { | ||||||
| @ -52,7 +57,7 @@ function calculateCidr({ startIp, endIp }: { startIp: string; endIp: string }) { | |||||||
|     const result: Ipv4RangeExpanderResult = {}; |     const result: Ipv4RangeExpanderResult = {}; | ||||||
|     result.newEnd = bits2ip(parseInt(cidr.end, 2)); |     result.newEnd = bits2ip(parseInt(cidr.end, 2)); | ||||||
|     result.newStart = bits2ip(parseInt(cidr.start, 2)); |     result.newStart = bits2ip(parseInt(cidr.start, 2)); | ||||||
|     result.newCidr = result.newStart + '/' + cidr.mask; |     result.newCidr = `${result.newStart}/${cidr.mask}`; | ||||||
|     result.newSize = getRangesize(cidr.start, cidr.end); |     result.newSize = getRangesize(cidr.start, cidr.end); | ||||||
| 
 | 
 | ||||||
|     result.oldSize = getRangesize(start, end); |     result.oldSize = getRangesize(start, end); | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| export type Ipv4RangeExpanderResult = { | export interface Ipv4RangeExpanderResult { | ||||||
|   oldSize?: number; |   oldSize?: number | ||||||
|   newStart?: string; |   newStart?: string | ||||||
|   newEnd?: string; |   newEnd?: string | ||||||
|   newCidr?: string; |   newCidr?: string | ||||||
|   newSize?: number; |   newSize?: number | ||||||
| }; | } | ||||||
|  | |||||||
| @ -1,3 +1,61 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { Exchange } from '@vicons/tabler'; | ||||||
|  | import { isValidIpv4 } from '../ipv4-address-converter/ipv4-address-converter.service'; | ||||||
|  | import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types'; | ||||||
|  | import { calculateCidr } from './ipv4-range-expander.service'; | ||||||
|  | import ResultRow from './result-row.vue'; | ||||||
|  | import { useValidation } from '@/composable/validation'; | ||||||
|  | 
 | ||||||
|  | const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1'); | ||||||
|  | const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255'); | ||||||
|  | 
 | ||||||
|  | const result = computed(() => calculateCidr({ startIp: rawStartAddress.value, endIp: rawEndAddress.value })); | ||||||
|  | 
 | ||||||
|  | const calculatedValues: { | ||||||
|  |   label: string | ||||||
|  |   getOldValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined | ||||||
|  |   getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined | ||||||
|  | }[] = [ | ||||||
|  |   { | ||||||
|  |     label: 'Start address', | ||||||
|  |     getOldValue: () => rawStartAddress.value, | ||||||
|  |     getNewValue: result => result?.newStart, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'End address', | ||||||
|  |     getOldValue: () => rawEndAddress.value, | ||||||
|  |     getNewValue: result => result?.newEnd, | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'Addresses in range', | ||||||
|  |     getOldValue: result => result?.oldSize?.toLocaleString(), | ||||||
|  |     getNewValue: result => result?.newSize?.toLocaleString(), | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label: 'CIDR', | ||||||
|  |     getOldValue: () => '', | ||||||
|  |     getNewValue: result => result?.newCidr, | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | const startIpValidation = useValidation({ | ||||||
|  |   source: rawStartAddress, | ||||||
|  |   rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }], | ||||||
|  | }); | ||||||
|  | const endIpValidation = useValidation({ | ||||||
|  |   source: rawEndAddress, | ||||||
|  |   rules: [{ message: 'Invalid ipv4 address', validator: ip => isValidIpv4({ ip }) }], | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined); | ||||||
|  | 
 | ||||||
|  | function onSwitchStartEndClicked() { | ||||||
|  |   const tmpStart = rawStartAddress.value; | ||||||
|  |   rawStartAddress.value = rawEndAddress.value; | ||||||
|  |   rawEndAddress.value = tmpStart; | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div> |   <div> | ||||||
|     <div mb-4 flex gap-4> |     <div mb-4 flex gap-4> | ||||||
| @ -21,13 +79,19 @@ | |||||||
|     <n-table v-if="showResult" data-test-id="result"> |     <n-table v-if="showResult" data-test-id="result"> | ||||||
|       <thead> |       <thead> | ||||||
|         <tr> |         <tr> | ||||||
|           <th scope="col"> </th> |           <th scope="col"> | ||||||
|           <th scope="col">old value</th> |   | ||||||
|           <th scope="col">new value</th> |           </th> | ||||||
|  |           <th scope="col"> | ||||||
|  |             old value | ||||||
|  |           </th> | ||||||
|  |           <th scope="col"> | ||||||
|  |             new value | ||||||
|  |           </th> | ||||||
|         </tr> |         </tr> | ||||||
|       </thead> |       </thead> | ||||||
|       <tbody> |       <tbody> | ||||||
|         <result-row |         <ResultRow | ||||||
|           v-for="{ label, getOldValue, getNewValue } in calculatedValues" |           v-for="{ label, getOldValue, getNewValue } in calculatedValues" | ||||||
|           :key="label" |           :key="label" | ||||||
|           :label="label" |           :label="label" | ||||||
| @ -53,62 +117,3 @@ | |||||||
|     </n-alert> |     </n-alert> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import { useValidation } from '@/composable/validation'; |  | ||||||
| import { Exchange } from '@vicons/tabler'; |  | ||||||
| import { isValidIpv4 } from '../ipv4-address-converter/ipv4-address-converter.service'; |  | ||||||
| import type { Ipv4RangeExpanderResult } from './ipv4-range-expander.types'; |  | ||||||
| import { calculateCidr } from './ipv4-range-expander.service'; |  | ||||||
| import ResultRow from './result-row.vue'; |  | ||||||
| 
 |  | ||||||
| const rawStartAddress = useStorage('ipv4-range-expander:startAddress', '192.168.1.1'); |  | ||||||
| const rawEndAddress = useStorage('ipv4-range-expander:endAddress', '192.168.6.255'); |  | ||||||
| 
 |  | ||||||
| const result = computed(() => calculateCidr({ startIp: rawStartAddress.value, endIp: rawEndAddress.value })); |  | ||||||
| 
 |  | ||||||
| const calculatedValues: { |  | ||||||
|   label: string; |  | ||||||
|   getOldValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined; |  | ||||||
|   getNewValue: (result: Ipv4RangeExpanderResult | undefined) => string | undefined; |  | ||||||
| }[] = [ |  | ||||||
|   { |  | ||||||
|     label: 'Start address', |  | ||||||
|     getOldValue: () => rawStartAddress.value, |  | ||||||
|     getNewValue: (result) => result?.newStart, |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     label: 'End address', |  | ||||||
|     getOldValue: () => rawEndAddress.value, |  | ||||||
|     getNewValue: (result) => result?.newEnd, |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     label: 'Addresses in range', |  | ||||||
|     getOldValue: (result) => result?.oldSize?.toLocaleString(), |  | ||||||
|     getNewValue: (result) => result?.newSize?.toLocaleString(), |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     label: 'CIDR', |  | ||||||
|     getOldValue: () => '', |  | ||||||
|     getNewValue: (result) => result?.newCidr, |  | ||||||
|   }, |  | ||||||
| ]; |  | ||||||
| 
 |  | ||||||
| const showResult = computed(() => endIpValidation.isValid && startIpValidation.isValid && result.value !== undefined); |  | ||||||
| const startIpValidation = useValidation({ |  | ||||||
|   source: rawStartAddress, |  | ||||||
|   rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }], |  | ||||||
| }); |  | ||||||
| const endIpValidation = useValidation({ |  | ||||||
|   source: rawEndAddress, |  | ||||||
|   rules: [{ message: 'Invalid ipv4 address', validator: (ip) => isValidIpv4({ ip }) }], |  | ||||||
| }); |  | ||||||
| 
 |  | ||||||
| function onSwitchStartEndClicked() { |  | ||||||
|   const tmpStart = rawStartAddress.value; |  | ||||||
|   rawStartAddress.value = rawEndAddress.value; |  | ||||||
|   rawEndAddress.value = tmpStart; |  | ||||||
| } |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped></style> |  | ||||||
|  | |||||||
| @ -1,18 +1,6 @@ | |||||||
| <template> |  | ||||||
|   <tr> |  | ||||||
|     <td> |  | ||||||
|       <n-text strong>{{ label }}</n-text> |  | ||||||
|     </td> |  | ||||||
|     <td :data-test-id="testId + '.old'"><span-copyable :value="oldValue" class="monospace" /></td> |  | ||||||
|     <td :data-test-id="testId + '.new'"> |  | ||||||
|       <span-copyable :value="newValue"></span-copyable> |  | ||||||
|     </td> |  | ||||||
|   </tr> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import SpanCopyable from '@/components/SpanCopyable.vue'; |  | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
|  | import SpanCopyable from '@/components/SpanCopyable.vue'; | ||||||
| 
 | 
 | ||||||
| const props = withDefaults(defineProps<{ label: string; oldValue?: string; newValue?: string }>(), { | const props = withDefaults(defineProps<{ label: string; oldValue?: string; newValue?: string }>(), { | ||||||
|   label: '', |   label: '', | ||||||
| @ -24,4 +12,18 @@ const { label, oldValue, newValue } = toRefs(props); | |||||||
| const testId = computed(() => _.kebabCase(label.value)); | const testId = computed(() => _.kebabCase(label.value)); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped lang="less"></style> | <template> | ||||||
|  |   <tr> | ||||||
|  |     <td> | ||||||
|  |       <n-text strong> | ||||||
|  |         {{ label }} | ||||||
|  |       </n-text> | ||||||
|  |     </td> | ||||||
|  |     <td :data-test-id="`${testId}.old`"> | ||||||
|  |       <SpanCopyable :value="oldValue" class="monospace" /> | ||||||
|  |     </td> | ||||||
|  |     <td :data-test-id="`${testId}.new`"> | ||||||
|  |       <SpanCopyable :value="newValue" /> | ||||||
|  |     </td> | ||||||
|  |   </tr> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,51 +1,12 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <c-input-text |  | ||||||
|       v-model:value="ip" |  | ||||||
|       label="An IPv4 address with or without mask" |  | ||||||
|       placeholder="The ipv4 address..." |  | ||||||
|       :validation-rules="ipValidationRules" |  | ||||||
|       mb-4 |  | ||||||
|     /> |  | ||||||
| 
 |  | ||||||
|     <div v-if="networkInfo"> |  | ||||||
|       <n-table> |  | ||||||
|         <tbody> |  | ||||||
|           <tr v-for="{ getValue, label, undefinedFallback } in sections" :key="label"> |  | ||||||
|             <td> |  | ||||||
|               <n-text strong>{{ label }}</n-text> |  | ||||||
|             </td> |  | ||||||
|             <td> |  | ||||||
|               <span-copyable v-if="getValue(networkInfo)" :value="getValue(networkInfo)"></span-copyable> |  | ||||||
|               <n-text v-else depth="3">{{ undefinedFallback }}</n-text> |  | ||||||
|             </td> |  | ||||||
|           </tr> |  | ||||||
|         </tbody> |  | ||||||
|       </n-table> |  | ||||||
| 
 |  | ||||||
|       <div mt-3 flex items-center justify-between> |  | ||||||
|         <c-button @click="switchToBlock({ count: -1 })"> |  | ||||||
|           <n-icon :component="ArrowLeft" /> |  | ||||||
|           Previous block |  | ||||||
|         </c-button> |  | ||||||
|         <c-button @click="switchToBlock({ count: 1 })"> |  | ||||||
|           Next block |  | ||||||
|           <n-icon :component="ArrowRight" /> |  | ||||||
|         </c-button> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { computed } from 'vue'; | import { computed } from 'vue'; | ||||||
| import { Netmask } from 'netmask'; | import { Netmask } from 'netmask'; | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; |  | ||||||
| import { isNotThrowing } from '@/utils/boolean'; |  | ||||||
| import { useStorage } from '@vueuse/core'; | import { useStorage } from '@vueuse/core'; | ||||||
| import { ArrowLeft, ArrowRight } from '@vicons/tabler'; | import { ArrowLeft, ArrowRight } from '@vicons/tabler'; | ||||||
| import SpanCopyable from '@/components/SpanCopyable.vue'; |  | ||||||
| import { getIPClass } from './ipv4-subnet-calculator.models'; | import { getIPClass } from './ipv4-subnet-calculator.models'; | ||||||
|  | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
|  | import { isNotThrowing } from '@/utils/boolean'; | ||||||
|  | import SpanCopyable from '@/components/SpanCopyable.vue'; | ||||||
| 
 | 
 | ||||||
| const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24'); | const ip = useStorage('ipv4-subnet-calculator:ip', '192.168.0.1/24'); | ||||||
| 
 | 
 | ||||||
| @ -61,13 +22,13 @@ const ipValidationRules = [ | |||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| const sections: { | const sections: { | ||||||
|   label: string; |   label: string | ||||||
|   getValue: (blocks: Netmask) => string | undefined; |   getValue: (blocks: Netmask) => string | undefined | ||||||
|   undefinedFallback?: string; |   undefinedFallback?: string | ||||||
| }[] = [ | }[] = [ | ||||||
|   { |   { | ||||||
|     label: 'Netmask', |     label: 'Netmask', | ||||||
|     getValue: (block) => block.toString(), |     getValue: block => block.toString(), | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     label: 'Network address', |     label: 'Network address', | ||||||
| @ -122,4 +83,45 @@ function switchToBlock({ count = 1 }: { count?: number }) { | |||||||
| } | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="less" scoped></style> | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-input-text | ||||||
|  |       v-model:value="ip" | ||||||
|  |       label="An IPv4 address with or without mask" | ||||||
|  |       placeholder="The ipv4 address..." | ||||||
|  |       :validation-rules="ipValidationRules" | ||||||
|  |       mb-4 | ||||||
|  |     /> | ||||||
|  | 
 | ||||||
|  |     <div v-if="networkInfo"> | ||||||
|  |       <n-table> | ||||||
|  |         <tbody> | ||||||
|  |           <tr v-for="{ getValue, label, undefinedFallback } in sections" :key="label"> | ||||||
|  |             <td> | ||||||
|  |               <n-text strong> | ||||||
|  |                 {{ label }} | ||||||
|  |               </n-text> | ||||||
|  |             </td> | ||||||
|  |             <td> | ||||||
|  |               <SpanCopyable v-if="getValue(networkInfo)" :value="getValue(networkInfo)" /> | ||||||
|  |               <n-text v-else depth="3"> | ||||||
|  |                 {{ undefinedFallback }} | ||||||
|  |               </n-text> | ||||||
|  |             </td> | ||||||
|  |           </tr> | ||||||
|  |         </tbody> | ||||||
|  |       </n-table> | ||||||
|  | 
 | ||||||
|  |       <div mt-3 flex items-center justify-between> | ||||||
|  |         <c-button @click="switchToBlock({ count: -1 })"> | ||||||
|  |           <n-icon :component="ArrowLeft" /> | ||||||
|  |           Previous block | ||||||
|  |         </c-button> | ||||||
|  |         <c-button @click="switchToBlock({ count: 1 })"> | ||||||
|  |           Next block | ||||||
|  |           <n-icon :component="ArrowRight" /> | ||||||
|  |         </c-button> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,36 +1,3 @@ | |||||||
| <template> |  | ||||||
|   <div> |  | ||||||
|     <n-alert title="Info" type="info"> |  | ||||||
|       This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed, |  | ||||||
|       and the lower 40 bits to generate your random ULA. |  | ||||||
|     </n-alert> |  | ||||||
| 
 |  | ||||||
|     <c-input-text |  | ||||||
|       v-model:value="macAddress" |  | ||||||
|       placeholder="Type a MAC address" |  | ||||||
|       clearable |  | ||||||
|       label="MAC address:" |  | ||||||
|       raw-text |  | ||||||
|       my-8 |  | ||||||
|       :validation="addressValidation" |  | ||||||
|     /> |  | ||||||
| 
 |  | ||||||
|     <div v-if="addressValidation.isValid"> |  | ||||||
|       <input-copyable |  | ||||||
|         v-for="{ label, value } in calculatedSections" |  | ||||||
|         :key="label" |  | ||||||
|         :value="value" |  | ||||||
|         :label="label" |  | ||||||
|         label-width="160px" |  | ||||||
|         label-align="right" |  | ||||||
|         label-position="left" |  | ||||||
|         readonly |  | ||||||
|         mb-2 |  | ||||||
|       /> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
| import { SHA1 } from 'crypto-js'; | import { SHA1 } from 'crypto-js'; | ||||||
| import InputCopyable from '@/components/InputCopyable.vue'; | import InputCopyable from '@/components/InputCopyable.vue'; | ||||||
| @ -43,7 +10,7 @@ const calculatedSections = computed(() => { | |||||||
|     .toString() |     .toString() | ||||||
|     .substring(30); |     .substring(30); | ||||||
| 
 | 
 | ||||||
|   const ula = 'fd' + hex40bit.substring(0, 2) + ':' + hex40bit.substring(2, 6) + ':' + hex40bit.substring(6); |   const ula = `fd${hex40bit.substring(0, 2)}:${hex40bit.substring(2, 6)}:${hex40bit.substring(6)}`; | ||||||
| 
 | 
 | ||||||
|   return [ |   return [ | ||||||
|     { |     { | ||||||
| @ -64,4 +31,35 @@ const calculatedSections = computed(() => { | |||||||
| const addressValidation = macAddressValidation(macAddress); | const addressValidation = macAddressValidation(macAddress); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="less" scoped></style> | <template> | ||||||
|  |   <div> | ||||||
|  |     <n-alert title="Info" type="info"> | ||||||
|  |       This tool uses the first method suggested by IETF using the current timestamp plus the mac address, sha1 hashed, | ||||||
|  |       and the lower 40 bits to generate your random ULA. | ||||||
|  |     </n-alert> | ||||||
|  | 
 | ||||||
|  |     <c-input-text | ||||||
|  |       v-model:value="macAddress" | ||||||
|  |       placeholder="Type a MAC address" | ||||||
|  |       clearable | ||||||
|  |       label="MAC address:" | ||||||
|  |       raw-text | ||||||
|  |       my-8 | ||||||
|  |       :validation="addressValidation" | ||||||
|  |     /> | ||||||
|  | 
 | ||||||
|  |     <div v-if="addressValidation.isValid"> | ||||||
|  |       <InputCopyable | ||||||
|  |         v-for="{ label, value } in calculatedSections" | ||||||
|  |         :key="label" | ||||||
|  |         :value="value" | ||||||
|  |         :label="label" | ||||||
|  |         label-width="160px" | ||||||
|  |         label-align="right" | ||||||
|  |         label-position="left" | ||||||
|  |         readonly | ||||||
|  |         mb-2 | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,16 +1,16 @@ | |||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
|  | import type { ArrayDifference, Difference, ObjectDifference } from '../json-diff.types'; | ||||||
| import { useCopy } from '@/composable/copy'; | import { useCopy } from '@/composable/copy'; | ||||||
| import type { Difference, ArrayDifference, ObjectDifference } from '../json-diff.types'; |  | ||||||
| 
 | 
 | ||||||
| export const DiffRootViewer = ({ diff }: { diff: Difference }) => { | export function DiffRootViewer({ diff }: { diff: Difference }) { | ||||||
|   return ( |   return ( | ||||||
|     <div class={'diffs-viewer'}> |     <div class={'diffs-viewer'}> | ||||||
|       <ul>{DiffViewer({ diff, showKeys: false })}</ul> |       <ul>{DiffViewer({ diff, showKeys: false })}</ul> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| const DiffViewer = ({ diff, showKeys = true }: { diff: Difference; showKeys?: boolean }) => { | function DiffViewer({ diff, showKeys = true }: { diff: Difference; showKeys?: boolean }) { | ||||||
|   const { type, status } = diff; |   const { type, status } = diff; | ||||||
| 
 | 
 | ||||||
|   if (status === 'updated') { |   if (status === 'updated') { | ||||||
| @ -26,9 +26,9 @@ const DiffViewer = ({ diff, showKeys = true }: { diff: Difference; showKeys?: bo | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return LineDiffViewer({ diff, showKeys }); |   return LineDiffViewer({ diff, showKeys }); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| const LineDiffViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) => { | function LineDiffViewer({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) { | ||||||
|   const { value, key, status, oldValue } = diff; |   const { value, key, status, oldValue } = diff; | ||||||
|   const valueToDisplay = status === 'removed' ? oldValue : value; |   const valueToDisplay = status === 'removed' ? oldValue : value; | ||||||
| 
 | 
 | ||||||
| @ -46,9 +46,9 @@ const LineDiffViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boole | |||||||
|       , |       , | ||||||
|     </li> |     </li> | ||||||
|   ); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| const ComparisonViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) => { | function ComparisonViewer({ diff, showKeys }: { diff: Difference; showKeys?: boolean }) { | ||||||
|   const { value, key, oldValue } = diff; |   const { value, key, oldValue } = diff; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
| @ -63,21 +63,21 @@ const ComparisonViewer = ({ diff, showKeys }: { diff: Difference; showKeys?: boo | |||||||
|       {Value({ value, status: 'added' })}, |       {Value({ value, status: 'added' })}, | ||||||
|     </li> |     </li> | ||||||
|   ); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| const ChildrenViewer = ({ | function ChildrenViewer({ | ||||||
|   diff, |   diff, | ||||||
|   openTag, |   openTag, | ||||||
|   closeTag, |   closeTag, | ||||||
|   showKeys, |   showKeys, | ||||||
|   showChildrenKeys = true, |   showChildrenKeys = true, | ||||||
| }: { | }: { | ||||||
|   diff: ArrayDifference | ObjectDifference; |   diff: ArrayDifference | ObjectDifference | ||||||
|   showKeys: boolean; |   showKeys: boolean | ||||||
|   showChildrenKeys?: boolean; |   showChildrenKeys?: boolean | ||||||
|   openTag: string; |   openTag: string | ||||||
|   closeTag: string; |   closeTag: string | ||||||
| }) => { | }) { | ||||||
|   const { children, key, status, type } = diff; |   const { children, key, status, type } = diff; | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
| @ -91,12 +91,12 @@ const ChildrenViewer = ({ | |||||||
|         )} |         )} | ||||||
| 
 | 
 | ||||||
|         {openTag} |         {openTag} | ||||||
|         {children.length > 0 && <ul>{children.map((diff) => DiffViewer({ diff, showKeys: showChildrenKeys }))}</ul>} |         {children.length > 0 && <ul>{children.map(diff => DiffViewer({ diff, showKeys: showChildrenKeys }))}</ul>} | ||||||
|         {closeTag + ','} |         {`${closeTag},`} | ||||||
|       </div> |       </div> | ||||||
|     </li> |     </li> | ||||||
|   ); |   ); | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| function formatValue(value: unknown) { | function formatValue(value: unknown) { | ||||||
|   if (_.isNull(value)) { |   if (_.isNull(value)) { | ||||||
| @ -106,7 +106,7 @@ function formatValue(value: unknown) { | |||||||
|   return JSON.stringify(value); |   return JSON.stringify(value); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const Value = ({ value, status }: { value: unknown; status: string }) => { | function Value({ value, status }: { value: unknown; status: string }) { | ||||||
|   const formatedValue = formatValue(value); |   const formatedValue = formatValue(value); | ||||||
| 
 | 
 | ||||||
|   const { copy } = useCopy({ source: formatedValue }); |   const { copy } = useCopy({ source: formatedValue }); | ||||||
| @ -116,4 +116,4 @@ const Value = ({ value, status }: { value: unknown; status: string }) => { | |||||||
|       {formatedValue} |       {formatedValue} | ||||||
|     </span> |     </span> | ||||||
|   ); |   ); | ||||||
| }; | } | ||||||
|  | |||||||
| @ -1,26 +1,11 @@ | |||||||
| <template> |  | ||||||
|   <div v-if="showResults"> |  | ||||||
|     <div flex justify-center> |  | ||||||
|       <n-form-item label="Only show differences" label-placement="left"> |  | ||||||
|         <n-switch v-model:value="onlyShowDifferences" /> |  | ||||||
|       </n-form-item> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|     <c-card data-test-id="diff-result"> |  | ||||||
|       <n-text v-if="jsonAreTheSame" depth="3" block text-center italic> The provided JSONs are the same </n-text> |  | ||||||
|       <diff-root-viewer v-else :diff="result" /> |  | ||||||
|     </c-card> |  | ||||||
|   </div> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import { useAppTheme } from '@/ui/theme/themes'; |  | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| import { DiffRootViewer } from './diff-viewer.models'; |  | ||||||
| import { diff } from '../json-diff.models'; | import { diff } from '../json-diff.models'; | ||||||
|  | import { DiffRootViewer } from './diff-viewer.models'; | ||||||
|  | import { useAppTheme } from '@/ui/theme/themes'; | ||||||
| 
 | 
 | ||||||
| const onlyShowDifferences = ref(false); |  | ||||||
| const props = defineProps<{ leftJson: unknown; rightJson: unknown }>(); | const props = defineProps<{ leftJson: unknown; rightJson: unknown }>(); | ||||||
|  | const onlyShowDifferences = ref(false); | ||||||
| const { leftJson, rightJson } = toRefs(props); | const { leftJson, rightJson } = toRefs(props); | ||||||
| const appTheme = useAppTheme(); | const appTheme = useAppTheme(); | ||||||
| 
 | 
 | ||||||
| @ -32,6 +17,23 @@ const jsonAreTheSame = computed(() => _.isEqual(leftJson.value, rightJson.value) | |||||||
| const showResults = computed(() => !_.isUndefined(leftJson.value) && !_.isUndefined(rightJson.value)); | const showResults = computed(() => !_.isUndefined(leftJson.value) && !_.isUndefined(rightJson.value)); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  | <template> | ||||||
|  |   <div v-if="showResults"> | ||||||
|  |     <div flex justify-center> | ||||||
|  |       <n-form-item label="Only show differences" label-placement="left"> | ||||||
|  |         <n-switch v-model:value="onlyShowDifferences" /> | ||||||
|  |       </n-form-item> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <c-card data-test-id="diff-result"> | ||||||
|  |       <n-text v-if="jsonAreTheSame" depth="3" block text-center italic> | ||||||
|  |         The provided JSONs are the same | ||||||
|  |       </n-text> | ||||||
|  |       <DiffRootViewer v-else :diff="result" /> | ||||||
|  |     </c-card> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| ::v-deep(.diffs-viewer) { | ::v-deep(.diffs-viewer) { | ||||||
|   color: v-bind('appTheme.text.mutedColor'); |   color: v-bind('appTheme.text.mutedColor'); | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { test, expect } from '@playwright/test'; | import { expect, test } from '@playwright/test'; | ||||||
| 
 | 
 | ||||||
| test.describe('Tool - JSON diff', () => { | test.describe('Tool - JSON diff', () => { | ||||||
|   test.beforeEach(async ({ page }) => { |   test.beforeEach(async ({ page }) => { | ||||||
| @ -24,7 +24,7 @@ test.describe('Tool - JSON diff', () => { | |||||||
| 
 | 
 | ||||||
|     const result = await page.getByTestId('diff-result').innerText(); |     const result = await page.getByTestId('diff-result').innerText(); | ||||||
| 
 | 
 | ||||||
|     expect(result).toContain(`{\nfoo: "bar""buz",\nbaz: "qux",\n},`); |     expect(result).toContain('{\nfoo: "bar""buz",\nbaz: "qux",\n},'); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   test('Different JSONs have only differences listed when "Only show differences" is checked', async ({ page }) => { |   test('Different JSONs have only differences listed when "Only show differences" is checked', async ({ page }) => { | ||||||
| @ -34,6 +34,6 @@ test.describe('Tool - JSON diff', () => { | |||||||
| 
 | 
 | ||||||
|     const result = await page.getByTestId('diff-result').innerText(); |     const result = await page.getByTestId('diff-result').innerText(); | ||||||
| 
 | 
 | ||||||
|     expect(result).toContain(`{\nbaz: "qux",\n},`); |     expect(result).toContain('{\nbaz: "qux",\n},'); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { expect, describe, it } from 'vitest'; | import { describe, expect, it } from 'vitest'; | ||||||
| import { diff } from './json-diff.models'; | import { diff } from './json-diff.models'; | ||||||
| 
 | 
 | ||||||
| describe('json-diff models', () => { | describe('json-diff models', () => { | ||||||
|  | |||||||
| @ -46,8 +46,8 @@ function diffObjects( | |||||||
| ): Difference[] { | ): Difference[] { | ||||||
|   const keys = Object.keys({ ...obj, ...newObj }); |   const keys = Object.keys({ ...obj, ...newObj }); | ||||||
|   return keys |   return keys | ||||||
|     .map((key) => createDifference(obj?.[key], newObj?.[key], key, { onlyShowDifferences })) |     .map(key => createDifference(obj?.[key], newObj?.[key], key, { onlyShowDifferences })) | ||||||
|     .filter((diff) => !onlyShowDifferences || diff.status !== 'unchanged'); |     .filter(diff => !onlyShowDifferences || diff.status !== 'unchanged'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function createDifference( | function createDifference( | ||||||
| @ -99,7 +99,7 @@ function diffArrays( | |||||||
|   const maxLength = Math.max(0, arr?.length, newArr?.length); |   const maxLength = Math.max(0, arr?.length, newArr?.length); | ||||||
|   return Array.from({ length: maxLength }, (_, i) => |   return Array.from({ length: maxLength }, (_, i) => | ||||||
|     createDifference(arr?.[i], newArr?.[i], i, { onlyShowDifferences }), |     createDifference(arr?.[i], newArr?.[i], i, { onlyShowDifferences }), | ||||||
|   ).filter((diff) => !onlyShowDifferences || diff.status !== 'unchanged'); |   ).filter(diff => !onlyShowDifferences || diff.status !== 'unchanged'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getType(value: unknown): 'object' | 'array' | 'value' { | function getType(value: unknown): 'object' | 'array' | 'value' { | ||||||
|  | |||||||
| @ -1,29 +1,29 @@ | |||||||
| export type DifferenceStatus = 'added' | 'removed' | 'updated' | 'unchanged' | 'children-updated'; | export type DifferenceStatus = 'added' | 'removed' | 'updated' | 'unchanged' | 'children-updated'; | ||||||
| 
 | 
 | ||||||
| export type ObjectDifference = { | export interface ObjectDifference { | ||||||
|   key: string | number; |   key: string | number | ||||||
|   type: 'object'; |   type: 'object' | ||||||
|   children: Difference[]; |   children: Difference[] | ||||||
|   status: DifferenceStatus; |   status: DifferenceStatus | ||||||
|   oldValue: unknown; |   oldValue: unknown | ||||||
|   value: unknown; |   value: unknown | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export type ValueDifference = { | export interface ValueDifference { | ||||||
|   key: string | number; |   key: string | number | ||||||
|   type: 'value'; |   type: 'value' | ||||||
|   value: unknown; |   value: unknown | ||||||
|   oldValue: unknown; |   oldValue: unknown | ||||||
|   status: DifferenceStatus; |   status: DifferenceStatus | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export type ArrayDifference = { | export interface ArrayDifference { | ||||||
|   key: number | string; |   key: number | string | ||||||
|   type: 'array'; |   type: 'array' | ||||||
|   children: Difference[]; |   children: Difference[] | ||||||
|   status: DifferenceStatus; |   status: DifferenceStatus | ||||||
|   oldValue: unknown; |   oldValue: unknown | ||||||
|   value: unknown; |   value: unknown | ||||||
| }; | } | ||||||
| 
 | 
 | ||||||
| export type Difference = ObjectDifference | ValueDifference | ArrayDifference; | export type Difference = ObjectDifference | ValueDifference | ArrayDifference; | ||||||
|  | |||||||
| @ -1,3 +1,24 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import JSON5 from 'json5'; | ||||||
|  | 
 | ||||||
|  | import DiffsViewer from './diff-viewer/diff-viewer.vue'; | ||||||
|  | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
|  | import { isNotThrowing } from '@/utils/boolean'; | ||||||
|  | 
 | ||||||
|  | const rawLeftJson = ref(''); | ||||||
|  | const rawRightJson = ref(''); | ||||||
|  | 
 | ||||||
|  | const leftJson = computed(() => withDefaultOnError(() => JSON5.parse(rawLeftJson.value), undefined)); | ||||||
|  | const rightJson = computed(() => withDefaultOnError(() => JSON5.parse(rawRightJson.value), undefined)); | ||||||
|  | 
 | ||||||
|  | const jsonValidationRules = [ | ||||||
|  |   { | ||||||
|  |     validator: (value: string) => value === '' || isNotThrowing(() => JSON5.parse(value)), | ||||||
|  |     message: 'Invalid JSON format', | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <c-input-text |   <c-input-text | ||||||
|     v-model:value="rawLeftJson" |     v-model:value="rawLeftJson" | ||||||
| @ -23,24 +44,3 @@ | |||||||
| 
 | 
 | ||||||
|   <DiffsViewer :left-json="leftJson" :right-json="rightJson" /> |   <DiffsViewer :left-json="leftJson" :right-json="rightJson" /> | ||||||
| </template> | </template> | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> |  | ||||||
| import JSON5 from 'json5'; |  | ||||||
| 
 |  | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; |  | ||||||
| import { isNotThrowing } from '@/utils/boolean'; |  | ||||||
| import DiffsViewer from './diff-viewer/diff-viewer.vue'; |  | ||||||
| 
 |  | ||||||
| const rawLeftJson = ref(''); |  | ||||||
| const rawRightJson = ref(''); |  | ||||||
| 
 |  | ||||||
| const leftJson = computed(() => withDefaultOnError(() => JSON5.parse(rawLeftJson.value), undefined)); |  | ||||||
| const rightJson = computed(() => withDefaultOnError(() => JSON5.parse(rawRightJson.value), undefined)); |  | ||||||
| 
 |  | ||||||
| const jsonValidationRules = [ |  | ||||||
|   { |  | ||||||
|     validator: (value: string) => value === '' || isNotThrowing(() => JSON5.parse(value)), |  | ||||||
|     message: 'Invalid JSON format', |  | ||||||
|   }, |  | ||||||
| ]; |  | ||||||
| </script> |  | ||||||
|  | |||||||
| @ -1,19 +1,7 @@ | |||||||
| <template> |  | ||||||
|   <format-transformer |  | ||||||
|     input-label="Your raw json" |  | ||||||
|     :input-default="defaultValue" |  | ||||||
|     input-placeholder="Paste your raw json here..." |  | ||||||
|     output-label="Minify version of your JSON" |  | ||||||
|     output-language="json" |  | ||||||
|     :input-validation-rules="rules" |  | ||||||
|     :transformer="transformer" |  | ||||||
|   /> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|  | import JSON5 from 'json5'; | ||||||
| import type { UseValidationRule } from '@/composable/validation'; | import type { UseValidationRule } from '@/composable/validation'; | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
| import JSON5 from 'json5'; |  | ||||||
| 
 | 
 | ||||||
| const defaultValue = '{\n\t"hello": [\n\t\t"world"\n\t]\n}'; | const defaultValue = '{\n\t"hello": [\n\t\t"world"\n\t]\n}'; | ||||||
| const transformer = (value: string) => withDefaultOnError(() => JSON.stringify(JSON5.parse(value), null, 0), ''); | const transformer = (value: string) => withDefaultOnError(() => JSON.stringify(JSON5.parse(value), null, 0), ''); | ||||||
| @ -25,3 +13,15 @@ const rules: UseValidationRule<string>[] = [ | |||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
| </script> | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <format-transformer | ||||||
|  |     input-label="Your raw json" | ||||||
|  |     :input-default="defaultValue" | ||||||
|  |     input-placeholder="Paste your raw json here..." | ||||||
|  |     output-label="Minify version of your JSON" | ||||||
|  |     output-language="json" | ||||||
|  |     :input-validation-rules="rules" | ||||||
|  |     :transformer="transformer" | ||||||
|  |   /> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { test, expect } from '@playwright/test'; | import { expect, test } from '@playwright/test'; | ||||||
| 
 | 
 | ||||||
| test.describe('Tool - json to yaml', () => { | test.describe('Tool - json to yaml', () => { | ||||||
|   test.beforeEach(async ({ page }) => { |   test.beforeEach(async ({ page }) => { | ||||||
| @ -14,6 +14,6 @@ test.describe('Tool - json to yaml', () => { | |||||||
| 
 | 
 | ||||||
|     const generatedJson = await page.getByTestId('area-content').innerText(); |     const generatedJson = await page.getByTestId('area-content').innerText(); | ||||||
| 
 | 
 | ||||||
|     expect(generatedJson.trim()).toEqual(`foo: bar\nlist:\n  - item\n  - key: value`.trim()); |     expect(generatedJson.trim()).toEqual('foo: bar\nlist:\n  - item\n  - key: value'.trim()); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -1,20 +1,9 @@ | |||||||
| <template> |  | ||||||
|   <format-transformer |  | ||||||
|     input-label="Your JSON" |  | ||||||
|     input-placeholder="Paste your JSON here..." |  | ||||||
|     output-label="YAML from your JSON" |  | ||||||
|     output-language="yaml" |  | ||||||
|     :input-validation-rules="rules" |  | ||||||
|     :transformer="transformer" |  | ||||||
|   /> |  | ||||||
| </template> |  | ||||||
| 
 |  | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|  | import { stringify } from 'yaml'; | ||||||
|  | import JSON5 from 'json5'; | ||||||
| import type { UseValidationRule } from '@/composable/validation'; | import type { UseValidationRule } from '@/composable/validation'; | ||||||
| import { isNotThrowing } from '@/utils/boolean'; | import { isNotThrowing } from '@/utils/boolean'; | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
| import { stringify } from 'yaml'; |  | ||||||
| import JSON5 from 'json5'; |  | ||||||
| 
 | 
 | ||||||
| const transformer = (value: string) => withDefaultOnError(() => stringify(JSON5.parse(value)), ''); | const transformer = (value: string) => withDefaultOnError(() => stringify(JSON5.parse(value)), ''); | ||||||
| 
 | 
 | ||||||
| @ -26,4 +15,13 @@ const rules: UseValidationRule<string>[] = [ | |||||||
| ]; | ]; | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="less" scoped></style> | <template> | ||||||
|  |   <format-transformer | ||||||
|  |     input-label="Your JSON" | ||||||
|  |     input-placeholder="Paste your JSON here..." | ||||||
|  |     output-label="YAML from your JSON" | ||||||
|  |     output-language="yaml" | ||||||
|  |     :input-validation-rules="rules" | ||||||
|  |     :transformer="transformer" | ||||||
|  |   /> | ||||||
|  | </template> | ||||||
|  | |||||||
| @ -1,3 +1,30 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, ref } from 'vue'; | ||||||
|  | import JSON5 from 'json5'; | ||||||
|  | import { useStorage } from '@vueuse/core'; | ||||||
|  | import { formatJson } from './json.models'; | ||||||
|  | import { withDefaultOnError } from '@/utils/defaults'; | ||||||
|  | import { useValidation } from '@/composable/validation'; | ||||||
|  | import TextareaCopyable from '@/components/TextareaCopyable.vue'; | ||||||
|  | 
 | ||||||
|  | const inputElement = ref<HTMLElement>(); | ||||||
|  | 
 | ||||||
|  | const rawJson = useStorage('json-prettify:raw-json', '{"hello": "world", "foo": "bar"}'); | ||||||
|  | const indentSize = useStorage('json-prettify:indent-size', 3); | ||||||
|  | const sortKeys = useStorage('json-prettify:sort-keys', true); | ||||||
|  | const cleanJson = computed(() => withDefaultOnError(() => formatJson({ rawJson, indentSize, sortKeys }), '')); | ||||||
|  | 
 | ||||||
|  | const rawJsonValidation = useValidation({ | ||||||
|  |   source: rawJson, | ||||||
|  |   rules: [ | ||||||
|  |     { | ||||||
|  |       validator: v => v === '' || JSON5.parse(v), | ||||||
|  |       message: 'Provided JSON is not valid.', | ||||||
|  |     }, | ||||||
|  |   ], | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
| <template> | <template> | ||||||
|   <div style="flex: 0 0 100%"> |   <div style="flex: 0 0 100%"> | ||||||
|     <div style="margin: 0 auto; max-width: 600px" flex justify-center gap-3> |     <div style="margin: 0 auto; max-width: 600px" flex justify-center gap-3> | ||||||
| @ -28,37 +55,10 @@ | |||||||
|     /> |     /> | ||||||
|   </n-form-item> |   </n-form-item> | ||||||
|   <n-form-item label="Prettify version of your json"> |   <n-form-item label="Prettify version of your json"> | ||||||
|     <textarea-copyable :value="cleanJson" language="json" :follow-height-of="inputElement" /> |     <TextareaCopyable :value="cleanJson" language="json" :follow-height-of="inputElement" /> | ||||||
|   </n-form-item> |   </n-form-item> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script setup lang="ts"> |  | ||||||
| import TextareaCopyable from '@/components/TextareaCopyable.vue'; |  | ||||||
| import { useValidation } from '@/composable/validation'; |  | ||||||
| import { withDefaultOnError } from '@/utils/defaults'; |  | ||||||
| import { computed, ref } from 'vue'; |  | ||||||
| import JSON5 from 'json5'; |  | ||||||
| import { useStorage } from '@vueuse/core'; |  | ||||||
| import { formatJson } from './json.models'; |  | ||||||
| 
 |  | ||||||
| const inputElement = ref<HTMLElement>(); |  | ||||||
| 
 |  | ||||||
| const rawJson = useStorage('json-prettify:raw-json', '{"hello": "world", "foo": "bar"}'); |  | ||||||
| const indentSize = useStorage('json-prettify:indent-size', 3); |  | ||||||
| const sortKeys = useStorage('json-prettify:sort-keys', true); |  | ||||||
| const cleanJson = computed(() => withDefaultOnError(() => formatJson({ rawJson, indentSize, sortKeys }), '')); |  | ||||||
| 
 |  | ||||||
| const rawJsonValidation = useValidation({ |  | ||||||
|   source: rawJson, |  | ||||||
|   rules: [ |  | ||||||
|     { |  | ||||||
|       validator: (v) => v === '' || JSON5.parse(v), |  | ||||||
|       message: 'Provided JSON is not valid.', |  | ||||||
|     }, |  | ||||||
|   ], |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
| 
 |  | ||||||
| <style lang="less" scoped> | <style lang="less" scoped> | ||||||
| .result-card { | .result-card { | ||||||
|   position: relative; |   position: relative; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { get, type MaybeRef } from '@vueuse/core'; | import { type MaybeRef, get } from '@vueuse/core'; | ||||||
| import JSON5 from 'json5'; | import JSON5 from 'json5'; | ||||||
| 
 | 
 | ||||||
| export { sortObjectKeys, formatJson }; | export { sortObjectKeys, formatJson }; | ||||||
| @ -25,9 +25,9 @@ function formatJson({ | |||||||
|   sortKeys = true, |   sortKeys = true, | ||||||
|   indentSize = 3, |   indentSize = 3, | ||||||
| }: { | }: { | ||||||
|   rawJson: MaybeRef<string>; |   rawJson: MaybeRef<string> | ||||||
|   sortKeys?: MaybeRef<boolean>; |   sortKeys?: MaybeRef<boolean> | ||||||
|   indentSize?: MaybeRef<number>; |   indentSize?: MaybeRef<number> | ||||||
| }) { | }) { | ||||||
|   const parsedObject = JSON5.parse(get(rawJson)); |   const parsedObject = JSON5.parse(get(rawJson)); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -38,9 +38,11 @@ function getFriendlyValue({ claim, value }: { claim: string; value: unknown }) { | |||||||
|     .otherwise(() => undefined); |     .otherwise(() => undefined); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const dateFormatter = (value: unknown) => { | function dateFormatter(value: unknown) { | ||||||
|   if (_.isNil(value)) return undefined; |   if (_.isNil(value)) { | ||||||
|  |     return undefined; | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   const date = new Date(Number(value) * 1000); |   const date = new Date(Number(value) * 1000); | ||||||
|   return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; |   return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; | ||||||
| }; | } | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user