From 37b0be9302e0e74750cda140be92c87ebcc04784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Sat, 1 Aug 2026 00:35:10 +0200 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=93=A6=EF=B8=8F=20Add=20types=20and?= =?UTF-8?q?=20linting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/.oxlintrc.json | 10 + frontend/index.html | 2 +- frontend/package.json | 16 +- frontend/pnpm-lock.yaml | 506 ++++++++++++++++-- frontend/pnpm-workspace.yaml | 3 + frontend/src/App.vue | 107 ++-- .../src/{chart.config.js => chart.config.ts} | 7 +- frontend/src/charts/BarChart.vue | 35 +- frontend/src/charts/DoughnutChart.vue | 44 +- frontend/src/charts/LineChart.vue | 47 +- frontend/src/elements/DividerHorizontal.vue | 2 +- frontend/src/elements/DropDown.vue | 2 +- frontend/src/elements/ModalDialog.vue | 2 +- frontend/src/elements/PrimaryButton.vue | 2 +- frontend/src/elements/SecondaryButton.vue | 2 +- frontend/src/elements/SongTag.vue | 2 +- frontend/src/elements/UserAvatar.vue | 2 +- frontend/src/elements/ZoneDanger.vue | 2 +- frontend/src/elements/ZoneInfo.vue | 2 +- frontend/src/keys.ts | 62 +++ frontend/src/{main.js => main.ts} | 21 +- frontend/src/modals/InfoSongData.vue | 30 -- frontend/src/modals/InfoSongSyntax.vue | 15 +- frontend/src/modals/PasswordChange.vue | 17 +- frontend/src/modals/SetlistDelete.vue | 15 +- frontend/src/modals/SetlistPresent.vue | 103 ++-- frontend/src/modals/SetlistSet.vue | 147 ++--- frontend/src/modals/SongAssign.vue | 13 +- frontend/src/modals/SongDelete.vue | 21 +- frontend/src/modals/SongPresent.vue | 47 +- frontend/src/modals/SongSet.vue | 105 ++-- frontend/src/modals/TagAssign.vue | 10 +- frontend/src/partials/LoginForm.vue | 17 +- frontend/src/partials/Logo.vue | 6 +- frontend/src/partials/ResetPassword.vue | 11 +- frontend/src/partials/SongContent.vue | 26 +- frontend/src/partials/SongFooter.vue | 10 +- frontend/src/{router.js => router.ts} | 0 frontend/src/{utils.js => utils.ts} | 113 ++-- frontend/src/views/Dashboard.vue | 19 +- frontend/src/views/Documentation.vue | 31 +- frontend/src/views/Profile.vue | 15 +- frontend/src/views/SetlistShow.vue | 244 +++++---- frontend/src/views/Setlists.vue | 76 +-- frontend/src/views/Shortcuts.vue | 13 +- frontend/src/views/SongShow.vue | 163 +++--- frontend/src/views/Songs.vue | 71 ++- frontend/src/vite-env.d.ts | 3 + frontend/src/widgets/WidgetSetlistList.vue | 12 +- frontend/src/widgets/WidgetSongList.vue | 30 +- frontend/src/widgets/WidgetSongOfYear.vue | 20 +- frontend/tsconfig.json | 24 + frontend/{vite.config.js => vite.config.ts} | 0 53 files changed, 1443 insertions(+), 862 deletions(-) create mode 100644 frontend/.oxlintrc.json rename frontend/src/{chart.config.js => chart.config.ts} (85%) create mode 100644 frontend/src/keys.ts rename frontend/src/{main.js => main.ts} (74%) delete mode 100644 frontend/src/modals/InfoSongData.vue rename frontend/src/{router.js => router.ts} (100%) rename frontend/src/{utils.js => utils.ts} (75%) create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.json rename frontend/{vite.config.js => vite.config.ts} (100%) diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json new file mode 100644 index 00000000..5818bd22 --- /dev/null +++ b/frontend/.oxlintrc.json @@ -0,0 +1,10 @@ +{ + "plugins": ["typescript", "vue"], + "categories": { + "correctness": "error" + }, + "rules": { + "typescript/no-explicit-any": "warn" + }, + "ignorePatterns": ["dist", "node_modules"] +} diff --git a/frontend/index.html b/frontend/index.html index bd658e32..082195ae 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -30,6 +30,6 @@
- + diff --git a/frontend/package.json b/frontend/package.json index 5ab1c92b..623f04bb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,8 +24,11 @@ "browser": "./dist/index.html", "scripts": { "dev": "vite", - "build": "vite build", - "serve": "vite preview" + "build": "vue-tsc --noEmit && vite build", + "serve": "vite preview", + "typecheck": "vue-tsc --noEmit", + "lint": "oxlint", + "lint:fix": "oxlint --fix" }, "dependencies": { "@kyvg/vue3-notification": "^3.1.3", @@ -51,9 +54,16 @@ "devDependencies": { "@tailwindcss/forms": "^0.5.10", "@tailwindcss/vite": "^4.0.14", + "@types/node": "^26.1.2", + "@types/pdfmake": "^0.2.13", "@vitejs/plugin-vue": "^5.0.3", + "@vue/tsconfig": "^0.9.1", + "oxlint": "^1.76.0", + "oxlint-tsgolint": "^7.0.2001", "tailwindcss": "^4.0.14", - "vite": "^6.0.7" + "typescript": "^6.0.3", + "vite": "^6.0.7", + "vue-tsc": "^3.3.9" }, "browserslist": [ "> 1%", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 821cc0e2..181d86df 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -10,19 +10,19 @@ importers: dependencies: '@kyvg/vue3-notification': specifier: ^3.1.3 - version: 3.4.2(vue@3.5.40) + version: 3.4.2(vue@3.5.40(typescript@6.0.3)) '@tabler/icons-vue': specifier: ^3.3.0 - version: 3.46.0(vue@3.5.40) + version: 3.46.0(vue@3.5.40(typescript@6.0.3)) '@vuepic/vue-datepicker': specifier: ^11.0.0 - version: 11.0.3(vue@3.5.40) + version: 11.0.3(vue@3.5.40(typescript@6.0.3)) '@vueuse/core': specifier: ^13.0.0 - version: 13.9.0(vue@3.5.40) + version: 13.9.0(vue@3.5.40(typescript@6.0.3)) '@vueuse/math': specifier: ^13.0.0 - version: 13.9.0(vue@3.5.40) + version: 13.9.0(vue@3.5.40(typescript@6.0.3)) '@zip.js/zip.js': specifier: ^2.7.45 version: 2.8.34 @@ -49,38 +49,59 @@ importers: version: 0.2.23 vue: specifier: ^3.2.37 - version: 3.5.40 + version: 3.5.40(typescript@6.0.3) vue-i18n: specifier: ^11.0.1 - version: 11.4.8(vue@3.5.40) + version: 11.4.8(vue@3.5.40(typescript@6.0.3)) vue-prism-editor: specifier: ^2.0.0-alpha.2 - version: 2.0.0-alpha.2(vue@3.5.40) + version: 2.0.0-alpha.2(vue@3.5.40(typescript@6.0.3)) vue-router: specifier: ^4.0.16 - version: 4.6.4(vue@3.5.40) + version: 4.6.4(vue@3.5.40(typescript@6.0.3)) vue3-carousel: specifier: ^0.15.0 - version: 0.15.1(vue@3.5.40) + version: 0.15.1(vue@3.5.40(typescript@6.0.3)) vuedraggable: specifier: ^4.1.0 - version: 4.1.0(vue@3.5.40) + version: 4.1.0(vue@3.5.40(typescript@6.0.3)) devDependencies: '@tailwindcss/forms': specifier: ^0.5.10 version: 0.5.11(tailwindcss@4.3.3) '@tailwindcss/vite': specifier: ^4.0.14 - version: 4.3.3(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)) + version: 4.3.3(vite@6.4.3(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.32.0)) + '@types/node': + specifier: ^26.1.2 + version: 26.1.2 + '@types/pdfmake': + specifier: ^0.2.13 + version: 0.2.13 '@vitejs/plugin-vue': specifier: ^5.0.3 - version: 5.2.4(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.40) + version: 5.2.4(vite@6.4.3(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.40(typescript@6.0.3)) + '@vue/tsconfig': + specifier: ^0.9.1 + version: 0.9.1(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3)) + oxlint: + specifier: ^1.76.0 + version: 1.76.0(oxlint-tsgolint@7.0.2001) + oxlint-tsgolint: + specifier: ^7.0.2001 + version: 7.0.2001 tailwindcss: specifier: ^4.0.14 version: 4.3.3 + typescript: + specifier: ^6.0.3 + version: 6.0.3 vite: specifier: ^6.0.7 - version: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) + version: 6.4.3(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.32.0) + vue-tsc: + specifier: ^3.3.9 + version: 3.3.9(typescript@6.0.3) packages: @@ -309,6 +330,158 @@ packages: peerDependencies: vue: ^3.0.0 + '@oxlint-tsgolint/darwin-arm64@7.0.2001': + resolution: {integrity: sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==} + cpu: [arm64] + os: [darwin] + + '@oxlint-tsgolint/darwin-x64@7.0.2001': + resolution: {integrity: sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw==} + cpu: [x64] + os: [darwin] + + '@oxlint-tsgolint/linux-arm64@7.0.2001': + resolution: {integrity: sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A==} + cpu: [arm64] + os: [linux] + + '@oxlint-tsgolint/linux-x64@7.0.2001': + resolution: {integrity: sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ==} + cpu: [x64] + os: [linux] + + '@oxlint-tsgolint/win32-arm64@7.0.2001': + resolution: {integrity: sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q==} + cpu: [arm64] + os: [win32] + + '@oxlint-tsgolint/win32-x64@7.0.2001': + resolution: {integrity: sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.76.0': + resolution: {integrity: sha512-ZHIE5Zt9AsPDcY4nOlofXt0YfneEeo+QrKMPcPzLf2Z6Q8VtV2W73d7SFJ920WUwyik783u/doKCs3KXdwG+7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.76.0': + resolution: {integrity: sha512-shm/ngQilHK6bs+ElJWa4oHfNj5vL1Gl/iVEJldTQjpr0/67oSgr0KUpbmcnLig5Fo0v/l6j2567A7TOL89ONA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.76.0': + resolution: {integrity: sha512-rvJmrAPKSQ9aWJ6wIS6CK2tJjwzfW0ApQH9qokq6sfDvmHwoyIHxHFMq7z7i7GiV6fdE6s8qvBqWKPTu8RmT6Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.76.0': + resolution: {integrity: sha512-U/zYdb7VYKGY6pA9Vd2rYl9O/HlCylcOlb5PGPvVLtg+oLGsk6H3XGKEMHKyqD3nmmtmlmwb/8SwU2vfSAtvMw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.76.0': + resolution: {integrity: sha512-WvKG9CAriuo0XNiFzpXjDngUZcRGFNpaK2kLyMUsnJlShxkT96u+BpJQ3KqdQwGOrvI14L6V8bAwXwAYNNY6Jg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.76.0': + resolution: {integrity: sha512-qJ5+RH99TqFRq3UCDxkW0zJJu9c+OAHFY72vGlxZLEpuO+MpKo3POgqb8sYipL9KYm8XY6ofb0HsOuvY6hQNqQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.76.0': + resolution: {integrity: sha512-PvPCVptkgVARsucgIqFQQcSmJ6xc6GtnVB5bRBekRahTc9eObMtjHfMjy5M+C2tHt5UCMttWM9RuSk/H9NqYeg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.76.0': + resolution: {integrity: sha512-3KeFDx8Bu4HPAXbuHZOr/oHvN+QT+JQhMw/NYPz7Z071xLSsG27Jfh9PIQVEY7hk1I+jr43ExqRIeJ6VKk2yLw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.76.0': + resolution: {integrity: sha512-oPFkkKTgl0K/EIg9fQ8oA3IGcI05/Mq1en04iFa41mmNPT+6KEiByVazTOZZJiHMBBrbsns1YJ2e1Scqwzesjw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.76.0': + resolution: {integrity: sha512-gN7yZ0eqflA5Fhf1wvHxGUltIV3FsvmB1zhNMDEK9vSHhc7E6qg9CuPeBgPZab66Tjzq6w6kHAtNEvnTHf4cyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.76.0': + resolution: {integrity: sha512-S/HqMbn22mQrjtErUxEoS/a55u8kIeXvreIxiJu5G7Le3UecEd6SQZxrDIpuhtgaFnsY/nVra3ytP+pRljDilA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.76.0': + resolution: {integrity: sha512-ZIga3097VJZolGZk6SrIAUokIGfRkxRlhiHDUznZptGBfwrhD7pNfD1rzEzsCwvk/1DX0A1bLz+liuNh5QKIVQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.76.0': + resolution: {integrity: sha512-ZGiiA7pFzMJSyMWYZTVlPgbTsx+Vl8ihLGMIujPwaslUF7kIPPWAbVmAlTc+9lWDV+DCiB8Ikixu+lSHeOIIWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.76.0': + resolution: {integrity: sha512-JLiy5WuvEBFTT6ErIFV35SLzi0R7Iri6MKU6dZbTxfIx8pndbbPs3Mj780nMipBFcPkti+okAPOJ9POKkHFEgg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.76.0': + resolution: {integrity: sha512-z7lgKQtbo/I1NIe8G5NHLesxJDv0tRSUWTpXKb9Pm3E9nKFKfO4IOSDtFroKgXtOYb0jQbcdH+0wzTyMXVes+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.76.0': + resolution: {integrity: sha512-JOjKymIpb9QcYfEhZsN6h4V9Ivd474W38cNIBRv6bg2TbIvogbMTH0Mg6YWW9TiRDqfcX+/Hyfsbo5vcSE5guQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.76.0': + resolution: {integrity: sha512-pqDWZiwcmByWUEm1NFUBNiT6aentCcaoMWJv0HbXEmuYermJ4sg8ppVrshubYP2MZ6SHccJJcpr6x469PuDFIw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.76.0': + resolution: {integrity: sha512-Ba0O659kgMv6pwO3z9PdO+K3aMxQRaw9HnG+e6AtOfgwcKFvYilciQYBoUBmxfQvOCKZe1SwjMkuB542NkuDMQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.76.0': + resolution: {integrity: sha512-5qcirPHO8nKfkoowEVWtpAoVTcYDy6g0UT0NGic450Qv8J2NrOqg4uQ8QppRP4MDTC7Xx47lbZnmadTH03CGGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rollup/rollup-android-arm-eabi@4.62.3': resolution: {integrity: sha512-c0wdcekXtQvvn5Tsrk/+op/gUArrbWaFduBnTLP2l1cKLSQs4diMWjJw3m6A0DdzT8dAAX95KpkJ3qynCePbmw==} cpu: [arm] @@ -557,6 +730,15 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/node@26.1.2': + resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} + + '@types/pdfkit@0.17.6': + resolution: {integrity: sha512-tIwzxk2uWKp0Cq9JIluQXJid77lYhF52EsIOwhsMF4iWLA6YneoBR1xVKYYdAysHuepUB0OX4tdwMiUDdGKmig==} + + '@types/pdfmake@0.2.13': + resolution: {integrity: sha512-QzOwk3dMTZOwVbDZNHGIQcpwBgprp1y1vzRBmR+p+vMtCZo00G1CN+YhNebkygtmaGjdzvyXJVOHMEuTbvhJ8A==} + '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} @@ -567,6 +749,15 @@ packages: vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + '@vue/compiler-core@3.5.40': resolution: {integrity: sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==} @@ -582,6 +773,9 @@ packages: '@vue/devtools-api@6.6.4': resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + '@vue/language-core@3.3.9': + resolution: {integrity: sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==} + '@vue/reactivity@3.5.40': resolution: {integrity: sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==} @@ -597,6 +791,17 @@ packages: '@vue/shared@3.5.40': resolution: {integrity: sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==} + '@vue/tsconfig@0.9.1': + resolution: {integrity: sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==} + peerDependencies: + typescript: '>= 5.8' + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + '@vuepic/vue-datepicker@11.0.3': resolution: {integrity: sha512-sb2adwqwK2PizLQOpxCYps2SwhVT6/ic2HMIOqHJXuYa6iAJZWGL5YVlS7O4aW+sk6ZyxlDURLO7kDZPL4HB/w==} engines: {node: '>=18.12.0'} @@ -625,6 +830,9 @@ packages: resolution: {integrity: sha512-+6a3lyqq69rpseLbvDPiVIWsZ/HdTGAAD6afFtug6ECPDGttb2dHnPC6cJgdPofYkzL9OvXizegq+DQVfL2rnA==} engines: {bun: '>=0.7.0', deno: '>=1.0.0', node: '>=18.0.0'} + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + amber-client@0.1.0-beta.1: resolution: {integrity: sha512-q2Q6WYZYVJRJlADjTOpG+7tGR7xWkDf2WCOXa8xpldT8NkXERiAfHMH4HwB0ktOPrJNWWXJgr4lcMdCnJzMCxA==} @@ -893,6 +1101,9 @@ packages: resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} hasBin: true + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + nanoid@3.3.16: resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -906,12 +1117,32 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + oxlint-tsgolint@7.0.2001: + resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==} + hasBin: true + + oxlint@1.76.0: + resolution: {integrity: sha512-6QoFioEU4fNdiUx/2Eo6TRd6NG7H7njnRCz8rhB66cZmMHDTqcm1Rjvl8Wry+ZTQMBAmyb4Mlf62Mk5X+eHSOw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=7.0.2001' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + pdfmake@0.2.23: resolution: {integrity: sha512-A/IksoKb/ikOZH1edSDJ/2zBbqJKDghD4+fXn3rT7quvCJDlsZMs3NmIB3eajLMMFU9Bd3bZPVvlUMXhvFI+bQ==} engines: {node: '>=18'} @@ -975,6 +1206,14 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + unicode-properties@1.4.1: resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} @@ -1021,6 +1260,9 @@ packages: yaml: optional: true + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-i18n@11.4.8: resolution: {integrity: sha512-0ULeHP6Z9CGvAm67S77ZEp41cfGXIREGL8qfhos2BMgcQQewtQcDKuojt6jjasAD/S8GwfTp2ySPmDSpwvrCMQ==} engines: {node: '>= 22'} @@ -1038,6 +1280,12 @@ packages: peerDependencies: vue: ^3.5.0 + vue-tsc@3.3.9: + resolution: {integrity: sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + vue3-carousel@0.15.1: resolution: {integrity: sha512-nV2ChLZtgAOikmls8WaVe37vO4Wagk4RYawePHIvXrFtduFnQODyzPx8MmXaAb6l+ka72yAi04ZeCyciiyJMyA==} peerDependencies: @@ -1218,9 +1466,84 @@ snapshots: '@kurkle/color@0.3.4': {} - '@kyvg/vue3-notification@3.4.2(vue@3.5.40)': + '@kyvg/vue3-notification@3.4.2(vue@3.5.40(typescript@6.0.3))': dependencies: - vue: 3.5.40 + vue: 3.5.40(typescript@6.0.3) + + '@oxlint-tsgolint/darwin-arm64@7.0.2001': + optional: true + + '@oxlint-tsgolint/darwin-x64@7.0.2001': + optional: true + + '@oxlint-tsgolint/linux-arm64@7.0.2001': + optional: true + + '@oxlint-tsgolint/linux-x64@7.0.2001': + optional: true + + '@oxlint-tsgolint/win32-arm64@7.0.2001': + optional: true + + '@oxlint-tsgolint/win32-x64@7.0.2001': + optional: true + + '@oxlint/binding-android-arm-eabi@1.76.0': + optional: true + + '@oxlint/binding-android-arm64@1.76.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.76.0': + optional: true + + '@oxlint/binding-darwin-x64@1.76.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.76.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.76.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.76.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.76.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.76.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.76.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.76.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.76.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.76.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.76.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.76.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.76.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.76.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.76.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.76.0': + optional: true '@rollup/rollup-android-arm-eabi@4.62.3': optional: true @@ -1297,10 +1620,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.3': optional: true - '@tabler/icons-vue@3.46.0(vue@3.5.40)': + '@tabler/icons-vue@3.46.0(vue@3.5.40(typescript@6.0.3))': dependencies: '@tabler/icons': 3.46.0 - vue: 3.5.40 + vue: 3.5.40(typescript@6.0.3) '@tabler/icons@3.46.0': {} @@ -1370,21 +1693,46 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))': + '@tailwindcss/vite@4.3.3(vite@6.4.3(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.32.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) + vite: 6.4.3(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.32.0) '@types/estree@1.0.9': {} + '@types/node@26.1.2': + dependencies: + undici-types: 8.3.0 + + '@types/pdfkit@0.17.6': + dependencies: + '@types/node': 26.1.2 + + '@types/pdfmake@0.2.13': + dependencies: + '@types/node': 26.1.2 + '@types/pdfkit': 0.17.6 + '@types/web-bluetooth@0.0.21': {} - '@vitejs/plugin-vue@5.2.4(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.40)': + '@vitejs/plugin-vue@5.2.4(vite@6.4.3(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.40(typescript@6.0.3))': dependencies: - vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0) - vue: 3.5.40 + vite: 6.4.3(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.32.0) + vue: 3.5.40(typescript@6.0.3) + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 '@vue/compiler-core@3.5.40': dependencies: @@ -1418,6 +1766,16 @@ snapshots: '@vue/devtools-api@6.6.4': {} + '@vue/language-core@3.3.9': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.40 + '@vue/shared': 3.5.40 + alien-signals: 3.2.1 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.5 + '@vue/reactivity@3.5.40': dependencies: '@vue/shared': 3.5.40 @@ -1442,31 +1800,38 @@ snapshots: '@vue/shared@3.5.40': {} - '@vuepic/vue-datepicker@11.0.3(vue@3.5.40)': + '@vue/tsconfig@0.9.1(typescript@6.0.3)(vue@3.5.40(typescript@6.0.3))': + optionalDependencies: + typescript: 6.0.3 + vue: 3.5.40(typescript@6.0.3) + + '@vuepic/vue-datepicker@11.0.3(vue@3.5.40(typescript@6.0.3))': dependencies: date-fns: 4.4.0 - vue: 3.5.40 + vue: 3.5.40(typescript@6.0.3) - '@vueuse/core@13.9.0(vue@3.5.40)': + '@vueuse/core@13.9.0(vue@3.5.40(typescript@6.0.3))': dependencies: '@types/web-bluetooth': 0.0.21 '@vueuse/metadata': 13.9.0 - '@vueuse/shared': 13.9.0(vue@3.5.40) - vue: 3.5.40 + '@vueuse/shared': 13.9.0(vue@3.5.40(typescript@6.0.3)) + vue: 3.5.40(typescript@6.0.3) - '@vueuse/math@13.9.0(vue@3.5.40)': + '@vueuse/math@13.9.0(vue@3.5.40(typescript@6.0.3))': dependencies: - '@vueuse/shared': 13.9.0(vue@3.5.40) - vue: 3.5.40 + '@vueuse/shared': 13.9.0(vue@3.5.40(typescript@6.0.3)) + vue: 3.5.40(typescript@6.0.3) '@vueuse/metadata@13.9.0': {} - '@vueuse/shared@13.9.0(vue@3.5.40)': + '@vueuse/shared@13.9.0(vue@3.5.40(typescript@6.0.3))': dependencies: - vue: 3.5.40 + vue: 3.5.40(typescript@6.0.3) '@zip.js/zip.js@2.8.34': {} + alien-signals@3.2.1: {} + amber-client@0.1.0-beta.1: {} base64-js@1.3.1: {} @@ -1724,6 +2089,8 @@ snapshots: mini-svg-data-uri@1.4.4: {} + muggle-string@0.4.1: {} + nanoid@3.3.16: {} object-is@1.1.6: @@ -1733,10 +2100,44 @@ snapshots: object-keys@1.1.1: {} + oxlint-tsgolint@7.0.2001: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 7.0.2001 + '@oxlint-tsgolint/darwin-x64': 7.0.2001 + '@oxlint-tsgolint/linux-arm64': 7.0.2001 + '@oxlint-tsgolint/linux-x64': 7.0.2001 + '@oxlint-tsgolint/win32-arm64': 7.0.2001 + '@oxlint-tsgolint/win32-x64': 7.0.2001 + + oxlint@1.76.0(oxlint-tsgolint@7.0.2001): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.76.0 + '@oxlint/binding-android-arm64': 1.76.0 + '@oxlint/binding-darwin-arm64': 1.76.0 + '@oxlint/binding-darwin-x64': 1.76.0 + '@oxlint/binding-freebsd-x64': 1.76.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.76.0 + '@oxlint/binding-linux-arm-musleabihf': 1.76.0 + '@oxlint/binding-linux-arm64-gnu': 1.76.0 + '@oxlint/binding-linux-arm64-musl': 1.76.0 + '@oxlint/binding-linux-ppc64-gnu': 1.76.0 + '@oxlint/binding-linux-riscv64-gnu': 1.76.0 + '@oxlint/binding-linux-riscv64-musl': 1.76.0 + '@oxlint/binding-linux-s390x-gnu': 1.76.0 + '@oxlint/binding-linux-x64-gnu': 1.76.0 + '@oxlint/binding-linux-x64-musl': 1.76.0 + '@oxlint/binding-openharmony-arm64': 1.76.0 + '@oxlint/binding-win32-arm64-msvc': 1.76.0 + '@oxlint/binding-win32-ia32-msvc': 1.76.0 + '@oxlint/binding-win32-x64-msvc': 1.76.0 + oxlint-tsgolint: 7.0.2001 + pako@0.2.9: {} pako@1.0.11: {} + path-browserify@1.0.1: {} + pdfmake@0.2.23: dependencies: '@foliojs-fork/linebreak': 1.1.2 @@ -1833,6 +2234,10 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 + typescript@6.0.3: {} + + undici-types@8.3.0: {} + unicode-properties@1.4.1: dependencies: base64-js: 1.5.1 @@ -1843,7 +2248,7 @@ snapshots: pako: 0.2.9 tiny-inflate: 1.0.3 - vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0): + vite@6.4.3(@types/node@26.1.2)(jiti@2.7.0)(lightningcss@1.32.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.5) @@ -1852,43 +2257,54 @@ snapshots: rollup: 4.62.3 tinyglobby: 0.2.17 optionalDependencies: + '@types/node': 26.1.2 fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.32.0 - vue-i18n@11.4.8(vue@3.5.40): + vscode-uri@3.1.0: {} + + vue-i18n@11.4.8(vue@3.5.40(typescript@6.0.3)): dependencies: '@intlify/core-base': 11.4.8 '@intlify/devtools-types': 11.4.8 '@intlify/shared': 11.4.8 '@vue/devtools-api': 6.6.4 - vue: 3.5.40 + vue: 3.5.40(typescript@6.0.3) - vue-prism-editor@2.0.0-alpha.2(vue@3.5.40): + vue-prism-editor@2.0.0-alpha.2(vue@3.5.40(typescript@6.0.3)): dependencies: - vue: 3.5.40 + vue: 3.5.40(typescript@6.0.3) - vue-router@4.6.4(vue@3.5.40): + vue-router@4.6.4(vue@3.5.40(typescript@6.0.3)): dependencies: '@vue/devtools-api': 6.6.4 - vue: 3.5.40 + vue: 3.5.40(typescript@6.0.3) - vue3-carousel@0.15.1(vue@3.5.40): + vue-tsc@3.3.9(typescript@6.0.3): dependencies: - vue: 3.5.40 + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.9 + typescript: 6.0.3 - vue@3.5.40: + vue3-carousel@0.15.1(vue@3.5.40(typescript@6.0.3)): + dependencies: + vue: 3.5.40(typescript@6.0.3) + + vue@3.5.40(typescript@6.0.3): dependencies: '@vue/compiler-dom': 3.5.40 '@vue/compiler-sfc': 3.5.40 '@vue/runtime-dom': 3.5.40 '@vue/server-renderer': 3.5.40 '@vue/shared': 3.5.40 + optionalDependencies: + typescript: 6.0.3 - vuedraggable@4.1.0(vue@3.5.40): + vuedraggable@4.1.0(vue@3.5.40(typescript@6.0.3)): dependencies: sortablejs: 1.14.0 - vue: 3.5.40 + vue: 3.5.40(typescript@6.0.3) xmldoc@2.0.3: dependencies: diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 5ed0b5af..2056fd34 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -1,2 +1,5 @@ allowBuilds: esbuild: true +minimumReleaseAgeExclude: + - '@vue/language-core@3.3.9' + - vue-tsc@3.3.9 diff --git a/frontend/src/App.vue b/frontend/src/App.vue index ee6d9e74..bd4f9d0b 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -201,19 +201,20 @@ :active="showModal.songset" :existing="songSetModalData.existing" :initial-song="songSetModalData.song" - :id="songSetModalData.id" + :id="songSetModalData.id ?? undefined" @closed="showModal.songset = false" /> - diff --git a/frontend/src/modals/InfoSongSyntax.vue b/frontend/src/modals/InfoSongSyntax.vue index aa395cdc..0085d222 100644 --- a/frontend/src/modals/InfoSongSyntax.vue +++ b/frontend/src/modals/InfoSongSyntax.vue @@ -10,8 +10,9 @@ - diff --git a/frontend/src/modals/PasswordChange.vue b/frontend/src/modals/PasswordChange.vue index bdbb2e8f..04112ab7 100644 --- a/frontend/src/modals/PasswordChange.vue +++ b/frontend/src/modals/PasswordChange.vue @@ -60,10 +60,11 @@ - diff --git a/frontend/src/modals/SongDelete.vue b/frontend/src/modals/SongDelete.vue index 1fbf69eb..18b36a5d 100644 --- a/frontend/src/modals/SongDelete.vue +++ b/frontend/src/modals/SongDelete.vue @@ -25,10 +25,11 @@ - diff --git a/frontend/src/partials/Logo.vue b/frontend/src/partials/Logo.vue index 86328047..25e8d789 100644 --- a/frontend/src/partials/Logo.vue +++ b/frontend/src/partials/Logo.vue @@ -23,13 +23,13 @@ - diff --git a/frontend/src/router.js b/frontend/src/router.ts similarity index 100% rename from frontend/src/router.js rename to frontend/src/router.ts diff --git a/frontend/src/utils.js b/frontend/src/utils.ts similarity index 75% rename from frontend/src/utils.js rename to frontend/src/utils.ts index d1554b0b..c088fe4d 100644 --- a/frontend/src/utils.js +++ b/frontend/src/utils.ts @@ -1,30 +1,32 @@ import { notify } from '@kyvg/vue3-notification'; +import type { AmberCollection } from 'amber-client'; +import type { Song, SongEntity } from '@backend/models'; import de from '@/locales/de.json'; import en from '@/locales/en.json'; // tag translations per locale, keyed by locale code -const tagTranslations = { de: de.tag, en: en.tag }; +const tagTranslations: Record> = { de: de.tag, en: en.tag }; // scale to use for song tuning and transponation const keyScale = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'B', 'H']; /* user roles and permissions: Administrator, Editor, Performer and Reader - A E P R + A E P R ------------------------------------------------------ - x x x x see dashboard, song list and setlist list - x x x x see single songs (present, tune, export) - x x x x see single setlists (present, copy, export) - x x x create and edit setlists - x x x custom song tuning in setlists - x x x synchronize setlists - x x create and edit songs - x x delete songs and setlists - x create, edit and delete song tags - x create, edit and delete song languages - x create, edit and delete users + x x x x see dashboard, song list and setlist list + x x x x see single songs (present, tune, export) + x x x x see single setlists (present, copy, export) + x x x create and edit setlists + x x x custom song tuning in setlists + x x x synchronize setlists + x x create and edit songs + x x delete songs and setlists + x create, edit and delete song tags + x create, edit and delete song languages + x create, edit and delete users */ -const userRoles = { +const userRoles: Record = { reader: 1, performer: 2, editor: 3, @@ -32,15 +34,25 @@ const userRoles = { }; // identify chord lines -const isChordLine = (line) => { +const isChordLine = (line: string): boolean => { if (line.trim() == '') return false; return line.slice(-2) === ' '; }; +// a single parsed part of a song's content (verse, chorus, ...) +export type SongPart = { + type: string; + number: number | string; + class: string; + content: string; +}; + // parse song content syntax -const parsedContent = (content, keyOffset, showChords, twoColumns) => { +function parsedContent(content: string, keyOffset: number, showChords: boolean, twoColumns: false): SongPart[]; +function parsedContent(content: string, keyOffset: number, showChords: boolean, twoColumns: true): [SongPart[], SongPart[]]; +function parsedContent(content: string, keyOffset: number, showChords: boolean, twoColumns: boolean): SongPart[] | [SongPart[], SongPart[]] { // initialize arrays for parsed lines, classes of parts, type abbr., numbers of type and part index - var parsed = [], classes = [], types = [], numbers = [], part = 0; + var parsed: string[][] = [], classes: string[] = [], types: string[] = [], numbers: (number | string)[] = [], part = 0; var lines = content.split('\n'); // check every single line of song content for (var i = 0; i < lines.length; i++) { @@ -159,7 +171,7 @@ const parsedContent = (content, keyOffset, showChords, twoColumns) => { part++; } } - var newContent = []; + var newContent: SongPart[] = []; // if multiple parts: rejoin lines of every part if (parsed.length > 1) { for (var p = 1; p < parsed.length; p++) { @@ -193,12 +205,12 @@ const parsedContent = (content, keyOffset, showChords, twoColumns) => { }; // file download -const download = (data, filename, isBlob = false) => { +const download = (data: string | Blob, filename: string, isBlob = false): void => { var a = document.createElement('a'); - var file = isBlob ? data : new Blob([data], { type:'text/plain;charset=UTF-8' }); + var file = isBlob ? data as Blob : new Blob([data as string], { type:'text/plain;charset=UTF-8' }); // IE10+ - if (window.navigator.msSaveOrOpenBlob) { - window.navigator.msSaveOrOpenBlob(file, filename); + if ((window.navigator as Navigator & { msSaveOrOpenBlob?: (blob: Blob, filename: string) => void }).msSaveOrOpenBlob) { + (window.navigator as Navigator & { msSaveOrOpenBlob?: (blob: Blob, filename: string) => void }).msSaveOrOpenBlob!(file, filename); } // other browsers else { @@ -215,9 +227,9 @@ const download = (data, filename, isBlob = false) => { }; // format human readable date -const humanDate = (d, locale, showWeekdate=true, short=false) => { +const humanDate = (d: string | Date | null | undefined, locale: string, showWeekdate = true, short = false): string => { if (!d) return ''; - let options = showWeekdate + let options: Intl.DateTimeFormatOptions = showWeekdate ? { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' } : { year: short ? '2-digit' : 'numeric', month: short ? 'numeric' : 'long', day: 'numeric' }; return (new Date(d)).toLocaleDateString(locale, options); @@ -225,13 +237,13 @@ const humanDate = (d, locale, showWeekdate=true, short=false) => { // format human readable file sizes // https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string#14919494 -const humanFileSize = (bytes, si=false, dp=1) => { +const humanFileSize = (bytes: number, si = false, dp = 1): string => { const thresh = si ? 1000 : 1024; if (Math.abs(bytes) < thresh) { return bytes + ' B'; } - const units = si - ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + const units = si + ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; let u = -1; const r = 10**dp; @@ -243,8 +255,8 @@ const humanFileSize = (bytes, si=false, dp=1) => { }; // highlight given text in SongDrive syntax highlighting -const sdHighlight = (code) => { - let parsedCode = []; +const sdHighlight = (code: string): string => { + let parsedCode: string[] = []; code.split('\n').forEach(line => { // check for marker if (line.substring(0,2) === '--') { @@ -263,14 +275,15 @@ const sdHighlight = (code) => { }; // calculate initials for given user name -const initials = (userName) => { +const initials = (userName?: string): string => { return userName ? userName.trim().split(' ', 2).reduce((p, c) => p + c.charAt(0), '').toUpperCase() : '' }; // toast error message -const throwError = (error) => { +export type ThrowableError = { code?: string; errorCode?: string; message?: string; error?: string }; +const throwError = (error: ThrowableError): void => { notify({ title: error.code ?? error.errorCode, text: error.message ?? error.error, @@ -279,23 +292,23 @@ const throwError = (error) => { }; // calculate random string e.g. as example password -const randomString = (length) => { +const randomString = (length: number): string => { let pass = '', rdm62; while (length--) { - // Generate random integer between 0 and 61, 0|x works for Math.floor(x) in this case - rdm62 = 0 | Math.random() * 62; + // Generate random integer between 0 and 61, 0|x works for Math.floor(x) in this case + rdm62 = 0 | Math.random() * 62; // Map to ascii codes: 0-9 to 48-57 (0-9), 10-35 to 65-90 (A-Z), 36-61 to 97-122 (a-z) - pass += String.fromCharCode(rdm62 + (rdm62 < 10 ? 48 : rdm62 < 36 ? 55 : 61)) + pass += String.fromCharCode(rdm62 + (rdm62 < 10 ? 48 : rdm62 < 36 ? 55 : 61)) } return pass; }; // make a given string url friendly -const urlify = (s) => { +const urlify = (s: string): string => { return s.trim().toLowerCase() .replace(/\s/g, '-') .replace(/\//g, '-') - .replace(/\_/g, '-') + .replace(/_/g, '-') .replace(/'/g, '') .replace(/"/g, '') .replace(/,/g, '') @@ -312,10 +325,10 @@ const urlify = (s) => { } // get the first key of given object that points to given value -const keyByValue = (o, v) => Object.keys(o).find(k => o[k]===v); +const keyByValue = (o: Record, v: unknown): string | undefined => Object.keys(o).find(k => o[k]===v); // sort tag keys by their translated name in the given locale -const sortTags = (tags, locale) => { +const sortTags = (tags: string[], locale: string): string[] => { const translations = tagTranslations[locale]; return tags.toSorted( (a, b) => (translations?.[a] ?? a).localeCompare(translations?.[b] ?? b) @@ -323,7 +336,7 @@ const sortTags = (tags, locale) => { }; // true if browser uses a dark color scheme -const browserPrefersDark = () => { +const browserPrefersDark = (): boolean => { return localStorage.theme === 'dark' || ( (!('theme' in localStorage) || localStorage.theme === 'auto') && window.matchMedia('(prefers-color-scheme: dark)').matches @@ -331,14 +344,23 @@ const browserPrefersDark = () => { }; // trigger mailto -const mailto = (address) => window.location.href = 'mailto:' + address; +const mailto = (address: string): void => { window.location.href = 'mailto:' + address; }; + +// vue-router route params can be a repeated segment (string[]); none of this +// app's routes use repeatable segments, so always take the first/only value +const firstParam = (v: string | string[] | undefined): string | undefined => Array.isArray(v) ? v[0] : v; // overwrite one target song's translations array via songsCollection, // preserving all its other fields (Amberbase updateDoc replaces the whole // document body, so the full current entity must be read and spread first) -const updateSongTranslations = async (songsCollection, songs, targetId, transformFn) => { +const updateSongTranslations = async ( + songsCollection: AmberCollection, + songs: Song[], + targetId: string, + transformFn: (translations: string[]) => string[], +): Promise => { const target = songs.find(s => s.id === targetId); - if (!target) return; // stale/unknown local reference - skip rather than throw + if (!target || target.changeNumber === undefined) return; // stale/unknown local reference - skip rather than throw await songsCollection.updateDoc(targetId, target.changeNumber, { ...target.entity, translations: transformFn(target.entity.translations ?? []), @@ -347,7 +369,7 @@ const updateSongTranslations = async (songsCollection, songs, targetId, transfor // build OpenLyrics XML for given song // see https://manual.openlp.org/display_tags.html#configuring-formatting-tags -const openLyricsXML = (song, version, translatedSong = null) => { +const openLyricsXML = (song: SongEntity, version: string, translatedSong: SongEntity | null = null): string => { const timestamp = (new Date()).toISOString().slice(0, -5); const title = `${song.title}`; const subtitle = song.subtitle ? `${song.subtitle}` : ''; @@ -370,7 +392,7 @@ const openLyricsXML = (song, version, translatedSong = null) => { const tParts = translatedSong ? parsedContent(translatedSong.content, 0, false, false) : []; const lyrics = parsedContent(song.content, 0, false, false).map((p, i) => { const type = p.type ? p.type.toUpperCase() : 'V'; - const num = p.number > 0 ? p.number : '1'; + const num = Number(p.number) > 0 ? p.number : '1'; const tContent = (i in tParts) ? `

${tParts[i].content.replace(/\n/g, "
")}
` : ''; @@ -397,6 +419,7 @@ export { sortTags, browserPrefersDark, mailto, + firstParam, updateSongTranslations, openLyricsXML, } diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index 6704ecc3..aaf6b7e9 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -88,8 +88,9 @@ -