From 80a63a320c04901b4a55557fe38c0c2c074ebd19 Mon Sep 17 00:00:00 2001 From: Pihkaal Date: Wed, 16 Oct 2024 02:01:39 +0200 Subject: [PATCH] refactor: split into component and use global app store --- app.vue | 387 +--------------------------- components/body/ApiModal.vue | 103 ++++++++ components/body/AppBody.vue | 9 + components/body/QRCodeForm.vue | 209 +++++++++++++++ components/body/QRCodePreview.vue | 10 + components/header/AppHeader.vue | 21 ++ components/header/ThemeSwitcher.vue | 28 ++ composables/useBaseUrl.ts | 1 + composables/useCopyable.ts | 25 ++ nuxt.config.ts | 8 +- package.json | 1 + pnpm-lock.yaml | 45 ++++ stores/app.ts | 6 + utils/formatting.ts | 7 + 14 files changed, 474 insertions(+), 386 deletions(-) create mode 100644 components/body/ApiModal.vue create mode 100644 components/body/AppBody.vue create mode 100644 components/body/QRCodeForm.vue create mode 100644 components/body/QRCodePreview.vue create mode 100644 components/header/AppHeader.vue create mode 100644 components/header/ThemeSwitcher.vue create mode 100644 composables/useBaseUrl.ts create mode 100644 composables/useCopyable.ts create mode 100644 stores/app.ts create mode 100644 utils/formatting.ts diff --git a/app.vue b/app.vue index 6a6c7e5..b700a7e 100644 --- a/app.vue +++ b/app.vue @@ -3,135 +3,6 @@ useSeoMeta({ title: "Simple QRCode Generator", description: "Simple, bullshit-free QR code generator.", }); - -import { renderQRCodeToCanvas } from "@/utils/renderer"; -import { IMAGE_FORMATS, LOGOS } from "@/utils/settings"; - -const form = ref(null); -const qrCode = ref(undefined); - -const copyUrlIcon = ref("i-heroicons-clipboard-document"); -const copyBaseApiUrlIcon = ref("i-heroicons-clipboard-document"); -const copyImageIcon = ref("i-heroicons-clipboard-document"); -const copyImageLabel = ref("Copy"); - -const isQRCodeEmpty = computed(() => !qrCode.value); -const qrCodeSrc = computed(() => qrCode.value ?? "/default.webp"); - -const isApiModelOpen = ref(false); - -const state = reactive({ - hasLogo: false, - logo: undefined, - format: IMAGE_FORMATS[0], - content: undefined, -}); - -const firstBlured = ref(false); - -const stateErrors = computed(() => ({ - content: firstBlured.value && !state.content, - logo: firstBlured.value && state.hasLogo && !state.logo, -})); - -const isValidState = computed( - () => - state.content && - ((state.hasLogo && state.logo) || !state.hasLogo) && - state.format, -); - -const BASE_API_URL = "https://simple-qr.com/api"; - -const apiUrl = computed(() => { - if (!isValidState.value) return ""; - - const params = new URLSearchParams({ - ...(state.hasLogo && { logo: state.logo }), - format: state.format, - content: state.content, - }); - - return `${BASE_API_URL}?${params}`; -}); - -const updateQRCode = async () => { - await nextTick(); - - if (!isValidState.value) return; - - const logoUrl = state.hasLogo ? `/logos/${state.logo}.png` : undefined; - const canvas = await renderQRCodeToCanvas(state.content, logoUrl); - - qrCode.value = canvas.toDataURL(`image/${state.format}`); -}; - -const copyUrl = async () => { - if (!isValidState.value) return; - - await navigator.clipboard.writeText(apiUrl.value); - - copyUrlIcon.value = "i-heroicons-clipboard-document-check"; - setTimeout(() => { - copyUrlIcon.value = "i-heroicons-clipboard-document"; - }, 3000); -}; - -const copyBaseApiUrl = async () => { - await navigator.clipboard.writeText(BASE_API_URL); - - copyBaseApiUrlIcon.value = "i-heroicons-clipboard-document-check"; - setTimeout(() => { - copyBaseApiUrlIcon.value = "i-heroicons-clipboard-document"; - }, 3000); -}; - -const downloadQRCode = () => { - const link = document.createElement("a"); - link.href = qrCode.value; - link.download = `qrcode.${state.format}`; - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); -}; - -const copyQRCode = async () => { - if (isQRCodeEmpty.value) return; - - const logoUrl = state.hasLogo ? `/logos/${state.logo}.png` : undefined; - const canvas = await renderQRCodeToCanvas(state.content, logoUrl); - - const qrCode = canvas.toDataURL(`image/png`); - - const blob = await (await fetch(qrCode)).blob(); - const item = new ClipboardItem({ "image/png": blob }); - await navigator.clipboard.write([item]); - - copyImageIcon.value = "i-heroicons-clipboard-document-check"; - copyImageLabel.value = "Copied!"; - setTimeout(() => { - copyImageIcon.value = "i-heroicons-clipboard-document"; - copyImageLabel.value = "Copy"; - }, 3000); -}; - -const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); - -const upperCase = (str: string) => str.toUpperCase(); - -const arrayToUnion = (array: string[]) => - array.map((x) => `"${x}"`).join(" | "); - -const colorMode = useColorMode(); -const isDark = computed({ - get() { - return colorMode.value === "dark"; - }, - set() { - colorMode.preference = colorMode.value === "dark" ? "light" : "dark"; - }, -}); diff --git a/components/body/ApiModal.vue b/components/body/ApiModal.vue new file mode 100644 index 0000000..c4482cc --- /dev/null +++ b/components/body/ApiModal.vue @@ -0,0 +1,103 @@ + + + diff --git a/components/body/AppBody.vue b/components/body/AppBody.vue new file mode 100644 index 0000000..a4b04c1 --- /dev/null +++ b/components/body/AppBody.vue @@ -0,0 +1,9 @@ + diff --git a/components/body/QRCodeForm.vue b/components/body/QRCodeForm.vue new file mode 100644 index 0000000..d16cb34 --- /dev/null +++ b/components/body/QRCodeForm.vue @@ -0,0 +1,209 @@ + + + diff --git a/components/body/QRCodePreview.vue b/components/body/QRCodePreview.vue new file mode 100644 index 0000000..6ac1274 --- /dev/null +++ b/components/body/QRCodePreview.vue @@ -0,0 +1,10 @@ + + + diff --git a/components/header/AppHeader.vue b/components/header/AppHeader.vue new file mode 100644 index 0000000..b0cbdf5 --- /dev/null +++ b/components/header/AppHeader.vue @@ -0,0 +1,21 @@ + diff --git a/components/header/ThemeSwitcher.vue b/components/header/ThemeSwitcher.vue new file mode 100644 index 0000000..ae50270 --- /dev/null +++ b/components/header/ThemeSwitcher.vue @@ -0,0 +1,28 @@ + + + diff --git a/composables/useBaseUrl.ts b/composables/useBaseUrl.ts new file mode 100644 index 0000000..e0c3611 --- /dev/null +++ b/composables/useBaseUrl.ts @@ -0,0 +1 @@ +export const useBaseApiUrl = () => `${useRequestURL().origin}/api`; diff --git a/composables/useCopyable.ts b/composables/useCopyable.ts new file mode 100644 index 0000000..b1e599a --- /dev/null +++ b/composables/useCopyable.ts @@ -0,0 +1,25 @@ +export const useCopyable = ( + valueOrCallback: string | Ref | (() => PromiseLike), +) => { + const icon = ref("i-heroicons-clipboard-document"); + const label = ref("Copy"); + + const copy = async () => { + if (typeof valueOrCallback === "function") { + await valueOrCallback(); + } else { + const value = unref(valueOrCallback); + await navigator.clipboard.writeText(value); + } + + icon.value = "i-heroicons-clipboard-document-check"; + label.value = "Copied!"; + + setTimeout(() => { + icon.value = "i-heroicons-clipboard-document"; + label.value = "Copy"; + }, 3000); + }; + + return { icon, copy, label }; +}; diff --git a/nuxt.config.ts b/nuxt.config.ts index 861652e..1f2fc0d 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -2,5 +2,11 @@ export default defineNuxtConfig({ compatibilityDate: "2024-04-03", devtools: { enabled: true }, - modules: ["@nuxt/eslint", "@nuxt/ui"], + modules: ["@nuxt/eslint", "@nuxt/ui", "@pinia/nuxt"], + components: [ + { + path: "~/components", + pathPrefix: false, + }, + ], }); diff --git a/package.json b/package.json index 4b98532..6e6a406 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@iconify-json/uil": "^1.2.1", "@nuxt/eslint": "^0.5.7", "@nuxt/ui": "^2.18.6", + "@pinia/nuxt": "^0.5.5", "canvas": "^2.11.2", "nuxt": "^3.13.0", "qrcode": "^1.5.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc2a99f..454321e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,9 @@ importers: "@nuxt/ui": specifier: ^2.18.6 version: 2.18.6(magicast@0.3.5)(qrcode@1.5.4)(rollup@4.22.5)(vite@5.4.8(@types/node@22.7.4)(terser@5.34.1))(vue@3.5.10(typescript@5.5.4)) + "@pinia/nuxt": + specifier: ^0.5.5 + version: 0.5.5(magicast@0.3.5)(rollup@4.22.5)(typescript@5.5.4)(vue@3.5.10(typescript@5.5.4)) canvas: specifier: ^2.11.2 version: 2.11.2 @@ -1700,6 +1703,12 @@ packages: } engines: { node: ">= 10.0.0" } + "@pinia/nuxt@0.5.5": + resolution: + { + integrity: sha512-wjxS7YqIesh4OLK+qE3ZjhdOJ5pYZQ+VlEmZNtTwzQn1Kavei/khovx7mzXVXNA/mvSPXVhb9xBzhyS3XMURtw==, + } + "@pkgjs/parseargs@0.11.0": resolution: { @@ -5676,6 +5685,21 @@ packages: } engines: { node: ">=0.10.0" } + pinia@2.2.4: + resolution: + { + integrity: sha512-K7ZhpMY9iJ9ShTC0cR2+PnxdQRuwVIsXDO/WIEV/RnMC/vmSoKDTKW/exNQYPI+4ij10UjXqdNiEHwn47McANQ==, + } + peerDependencies: + "@vue/composition-api": ^1.4.0 + typescript: ">=4.4.4" + vue: ^2.6.14 || ^3.3.0 + peerDependenciesMeta: + "@vue/composition-api": + optional: true + typescript: + optional: true + pirates@4.0.6: resolution: { @@ -8782,6 +8806,19 @@ snapshots: "@parcel/watcher-win32-ia32": 2.4.1 "@parcel/watcher-win32-x64": 2.4.1 + "@pinia/nuxt@0.5.5(magicast@0.3.5)(rollup@4.22.5)(typescript@5.5.4)(vue@3.5.10(typescript@5.5.4))": + dependencies: + "@nuxt/kit": 3.13.2(magicast@0.3.5)(rollup@4.22.5) + pinia: 2.2.4(typescript@5.5.4)(vue@3.5.10(typescript@5.5.4)) + transitivePeerDependencies: + - "@vue/composition-api" + - magicast + - rollup + - supports-color + - typescript + - vue + - webpack-sources + "@pkgjs/parseargs@0.11.0": optional: true @@ -11384,6 +11421,14 @@ snapshots: pify@2.3.0: {} + pinia@2.2.4(typescript@5.5.4)(vue@3.5.10(typescript@5.5.4)): + dependencies: + "@vue/devtools-api": 6.6.4 + vue: 3.5.10(typescript@5.5.4) + vue-demi: 0.14.10(vue@3.5.10(typescript@5.5.4)) + optionalDependencies: + typescript: 5.5.4 + pirates@4.0.6: {} pkg-types@1.2.0: diff --git a/stores/app.ts b/stores/app.ts new file mode 100644 index 0000000..ce7a447 --- /dev/null +++ b/stores/app.ts @@ -0,0 +1,6 @@ +export const useAppStore = defineStore("appStore", { + state: () => ({ + qrCode: "/default.webp", + apiModalOpened: false, + }), +}); diff --git a/utils/formatting.ts b/utils/formatting.ts new file mode 100644 index 0000000..942dc20 --- /dev/null +++ b/utils/formatting.ts @@ -0,0 +1,7 @@ +export const capitalize = (str: string) => + str.charAt(0).toUpperCase() + str.slice(1); + +export const upperCase = (str: string) => str.toUpperCase(); + +export const arrayToUnion = (array: string[]) => + array.map((x) => `"${x}"`).join(" | ");