139 Commits

Author SHA1 Message Date
jakob.scheid 1473dcf060 test(settings): add test cases for the selection setting allowMultiple property to the settings validator unit test 2026-06-04 17:27:42 +02:00
jakob.scheid 8702648624 test(settings): add unit test for the settings validator validateSettingsConfig function 2026-06-04 17:27:42 +02:00
jakob.scheid 88819f5684 test(settings): add test cases for the settings validator validateSelectionOptions function for test cases that should throw an error 2026-06-04 17:27:42 +02:00
jakob.scheid 98940eeec4 test(settings): add test cases for the settings validator validateEntry function for test cases that should throw an error 2026-06-04 17:27:42 +02:00
jakob.scheid 775e206322 fix(settings): check selection default value in the settings validator 2026-06-04 17:27:42 +02:00
jakob.scheid 7407366f45 fix(settings): check the selection options and allowMultiple before and not after the default check in the settings validator 2026-06-04 17:27:42 +02:00
jakob.scheid deaf3935c9 fix(settings): fix indentation in validateEntry in the settings validator 2026-06-04 17:27:42 +02:00
jakob.scheid 5f07e66915 fix(settings): check the selection options and allowMultiple after and not in the default check in the settings validator 2026-06-04 17:27:42 +02:00
jakob.scheid 122663e2e3 fix(settings): check the name and i18n before the checks for a section in validateEntry in the settings validator 2026-06-04 17:27:42 +02:00
jakob.scheid 5c8073d264 test(settings): add unit test for the settings validator validateEntry function for test cases that should throw an error 2026-06-04 17:27:42 +02:00
jakob.scheid 970f4a74d1 fix(settings): do not enforce allowMultiple set for selection settings 2026-06-04 17:27:42 +02:00
jakob.scheid ecf9074522 test(settings): add unit test for the settings validator validateEntry function for test cases that should not throw an error 2026-06-04 17:27:42 +02:00
jakob.scheid ea87e0832a test(settings): add unit test for the settings validator validateSelectionOptions function for test cases that should throw an error 2026-06-04 17:27:42 +02:00
jakob.scheid f4121bf419 test(settings): add unit test for the settings validator validateSelectionOptions function for test cases that should not throw an error 2026-06-04 17:27:42 +02:00
jakob.scheid bf317eea35 test(settings): update test description for the unit test for the settings validator assertString function for test cases that should throw an error 2026-06-04 17:27:42 +02:00
jakob.scheid 948d6d41e8 test(settings): add unit test for the settings validator assertType function for test cases that should throw an error 2026-06-04 17:27:42 +02:00
jakob.scheid 867b3a41f8 test(settings): add unit test for the settings validator assertType function for test cases that should not throw an error 2026-06-04 17:27:42 +02:00
jakob.scheid e0268b6e6c test(settings): add unit test for the settings validator assertString function for test cases that should not throw an error 2026-06-04 17:27:42 +02:00
jakob.scheid 156b3b552c test(settings): add unit test for the settings validator assertString function for test cases that should throw an error 2026-06-04 17:27:42 +02:00
jakob.scheid 39e6c94d09 test(settings): Add settings validator utility test boilerplate 2026-06-04 17:27:42 +02:00
jakob.scheid 76fda9e5c6 Merge pull request 'Fix centering of search bar on small screens' (#112) from bug/center-searchbar into main
Deploy on dev / Deploy on dev (push) Successful in 37s
Reviewed-on: #112
Reviewed-by: Jakob Scheid
2026-06-04 15:33:15 +02:00
jakob.scheid d9c19b252e fix(search-bar): re-add left padding 2026-06-04 15:32:46 +02:00
johannes.vos 91fa06898f style(searchbar): apply internal text indentation to the input field 2026-06-04 14:19:34 +02:00
johannes.vos 6d7dd7dc40 style(searchbar): make wrapper padding symmetric and set width to 100% 2026-06-04 14:19:04 +02:00
jakob.scheid 5122d93072 Merge pull request 'Language switch button unit test' (#108) from testing/language-switch into main
Deploy on dev / Deploy on dev (push) Successful in 38s
Reviewed-on: #108
Reviewed-by: Jakob Scheid
2026-06-04 13:46:17 +02:00
jakob.scheid dc4c47e2ef test(language-switch-button): remove redundant test file 2026-06-04 13:45:22 +02:00
jakob.scheid a5dede3485 Merge remote-tracking branch 'refs/remotes/origin/testing/language-switch' into testing/language-switch 2026-06-04 13:44:26 +02:00
jakob.scheid eb892d9725 test(language-switch-button): move test file to the correct directory 2026-06-04 13:43:55 +02:00
johannes.vos 27f696247d test: move language switch test, fix header comments and code indentation 2026-06-04 13:43:07 +02:00
johannes.vos 3a07b0482f chore: add @vue/test-utils to devDependencies 2026-06-04 13:43:07 +02:00
johannes.vos 88634e10cd validate dropdown closes on click outside the component 2026-06-04 13:43:07 +02:00
johannes.vos 98cc6b7556 assert language selection updates localStorage and HTML attributes 2026-06-04 13:43:07 +02:00
johannes.vos 0bb0f23347 verify initial closed state and toggle opening of dropdown 2026-06-04 13:43:07 +02:00
johannes.vos 02af96a07d initialize language switch test suite with beforeEach setup 2026-06-04 13:43:07 +02:00
johannes.vos a6194bb874 mock i18n and icon dependencies for language switch 2026-06-04 13:43:07 +02:00
johannes.vos d6700d6965 add imports 2026-06-04 13:43:07 +02:00
johannes.vos 54ffaf73e3 add license 2026-06-04 13:43:07 +02:00
johannes.vos ebe21f7f4d add LanguageSwitchButton.test.js file - empty 2026-06-04 13:43:07 +02:00
jakob.scheid 37515268d7 Merge pull request 'Fix navbar alignment on mobile devices' (#110) from feature/fix-navbar-alignment-on-mobile-devices into main
Deploy on dev / Deploy on dev (push) Successful in 32s
Reviewed-on: #110
Reviewed-by: Jakob Gregory <7+jakob.gregory@noreply.localhost>
Reviewed-by: Jakob Scheid
2026-06-04 13:40:29 +02:00
jakob.scheid c919263c36 feat(navbar-padding): use the responsivity of the main content padding CSS variables 2026-06-04 13:39:58 +02:00
jakob.scheid 592eb7222f move main content padding CSS variables to the app wrapper 2026-06-04 13:39:58 +02:00
johannes.vos 98579b726b style(nav): reduce navbar horizontal padding on small screens for better alignment 2026-06-04 13:39:58 +02:00
johannes.vos 918d6768e8 test: move language switch test, fix header comments and code indentation 2026-06-04 13:34:48 +02:00
johannes.vos 924698df24 chore: add @vue/test-utils to devDependencies 2026-06-04 13:33:46 +02:00
jakob.scheid 4e6c94afc8 Merge pull request 'Fix spacing between Seekra logo and slogan' (#109) from feature/vertical-spacing-slogan-logo into main
Deploy on dev / Deploy on dev (push) Successful in 32s
Reviewed-on: #109
Reviewed-by: Jakob Gregory
2026-06-04 13:22:17 +02:00
johannes.vos f2f7e9e23c remove unnecesary empty code lines 2026-06-04 13:16:31 +02:00
johannes.vos 08949d5bb4 remove @media (max-width: 48rem) - unneccesary 2026-06-04 13:13:58 +02:00
johannes.vos 65153db414 style(search): implement responsive mobile spacing for slogan via media query 2026-06-04 13:11:55 +02:00
johannes.vos 58e4640965 style(search): add desktop vertical spacing and fix line-height for slogan 2026-06-04 13:10:43 +02:00
johannes.vos 521f1becb4 validate dropdown closes on click outside the component 2026-06-04 12:59:59 +02:00
johannes.vos dc1caac2ed assert language selection updates localStorage and HTML attributes 2026-06-04 12:59:41 +02:00
johannes.vos e8cb978a61 verify initial closed state and toggle opening of dropdown 2026-06-04 12:59:08 +02:00
johannes.vos 1c3175a3e4 initialize language switch test suite with beforeEach setup 2026-06-04 12:58:47 +02:00
johannes.vos a97ee3c660 mock i18n and icon dependencies for language switch 2026-06-04 12:58:20 +02:00
johannes.vos baa1f6ed44 add imports 2026-06-04 12:56:46 +02:00
johannes.vos d5b76b0a17 add license 2026-06-04 12:51:24 +02:00
johannes.vos 5c3d7ce535 add LanguageSwitchButton.test.js file - empty 2026-06-04 12:50:35 +02:00
jakob.scheid 072f3472f7 Merge pull request 'Add current language utility unit test' (#106) from testing/current-language-util into main
Deploy on dev / Deploy on dev (push) Successful in 30s
Reviewed-on: #106
Reviewed-by: Johannes D. Vos
2026-06-04 12:41:54 +02:00
jakob.scheid 7d15d29409 test(current-language-util): shorten test description 2026-06-04 12:41:25 +02:00
jakob.scheid 8ce315e1c7 test(current-language-util): add local storage language test cases 2026-06-04 12:41:25 +02:00
jakob.scheid 33dfee951a test(current-language-util): set local storage item 'locale' to the local storage language if it is specified 2026-06-04 12:41:25 +02:00
jakob.scheid 80bc8339ed test(current-language-util): add test cases localStorageLanguage key 2026-06-04 12:41:25 +02:00
jakob.scheid 12f9d85841 test(current-language-util): remove local storage language test cases 2026-06-04 12:41:25 +02:00
jakob.scheid a78fa93b51 test(current-language-util): use objects for test cases 2026-06-04 12:41:25 +02:00
jakob.scheid 06c5e9b2c1 test(current-language-util): add local storage languages array 2026-06-04 12:41:25 +02:00
jakob.scheid bf3e6b2e2e refactor(current-language-util): move the locales array to a separate const in the test 2026-06-04 12:41:25 +02:00
jakob.scheid b58ee95ad2 fix(current-language-util): use the full locale instead of the language for the testing environment 2026-06-04 12:41:25 +02:00
jakob.scheid 093d5840fc test(current-language-util): update test description 2026-06-04 12:41:25 +02:00
jakob.scheid 352bd5baf5 test(current-language-util): add actual test 2026-06-04 12:41:25 +02:00
jakob.scheid 2117c28606 test(current-language-util): add navigator.language configuration 2026-06-04 12:41:25 +02:00
jakob.scheid 7f01a807b9 test(current-language-util): add test cases with extension 2026-06-04 12:41:25 +02:00
jakob.scheid a7babaf94b test(current-language-util): add test cases with writing system 2026-06-04 12:41:25 +02:00
jakob.scheid a14318c4c5 test(current-language-util): add test cases with country code 2026-06-04 12:41:25 +02:00
jakob.scheid c3c7fdb945 test(current-language-util): add simple test cases 2026-06-04 12:41:25 +02:00
jakob.scheid 1418e52cbf test(current-language-util): add test boilerplate 2026-06-04 12:41:25 +02:00
jakob.scheid 2d3d2457cf Merge pull request 'Add CSS dimensions utility unit test' (#107) from testing/css-dimensions-util into main
Deploy on dev / Deploy on dev (push) Successful in 36s
Reviewed-on: #107
Reviewed-by: Johannes D. Vos
2026-06-04 12:41:14 +02:00
jakob.scheid 6c12550ee7 test(css-dimensions-util): add test cases with complex dimensions 2026-06-04 08:55:59 +02:00
jakob.scheid 9ec2d7fd6f test(css-dimensions-util): add test cases with numbers and units as dimensions 2026-06-04 08:50:15 +02:00
jakob.scheid 975e84be46 test(css-dimensions-util): add test cases with numbers as dimensions 2026-06-04 08:50:04 +02:00
jakob.scheid a365322a5b test(css-dimensions-util): add test cases with empty dimensions 2026-06-03 23:43:48 +02:00
jakob.scheid 67fc878575 test(css-dimensions-util): add actual test 2026-06-03 23:40:59 +02:00
jakob.scheid 008e38e54a test(css-dimensions-util): add test boilerplate 2026-06-03 23:40:14 +02:00
jakob.scheid a0d592d26e test(css-dimensions-util): add test boilerplate 2026-06-03 23:38:04 +02:00
jakob.scheid 0b6adfa41d test(css-dimensions-util): add empty test file 2026-06-03 23:36:17 +02:00
jakob.scheid dc557afbc2 Merge pull request 'Set main content bottom padding' (#104) from feature/main-content-margin-bottom into main
Deploy on dev / Deploy on dev (push) Successful in 33s
Reviewed-on: #104
Reviewed-by: Jakob Gregory
2026-06-02 21:07:51 +02:00
jakob.scheid 88d419fd17 feat(main-content-bottom-margin): remove main content class bottom padding 0 2026-06-02 17:41:24 +02:00
jakob.scheid 37bba31fd4 Merge pull request 'Position the search results error message statically instead of absolutely' (#101) from fix/search-results-error-message into main
Deploy on dev / Deploy on dev (push) Successful in 34s
Reviewed-on: #101
Reviewed-by: Johannes D. Vos
2026-06-02 13:19:35 +02:00
jakob.scheid ca70d46df3 fix(search-results-error-message): fix width 2026-06-01 18:54:13 +02:00
jakob.scheid 2a0b5c9b86 fix(search-results-error-message): remove absolute positioning 2026-06-01 18:54:13 +02:00
jakob.scheid 316a8fd56f Merge pull request 'Move language switch button to the footer' (#100) from feature/language-switch-in-footer into main
Deploy on dev / Deploy on dev (push) Successful in 34s
Reviewed-on: #100
Reviewed-by: Johannes D. Vos
2026-06-01 18:50:48 +02:00
jakob.scheid 7bf44ca720 feat(language-switch-in-footer): show the dropdown menu above instead of below the button 2026-06-01 18:50:14 +02:00
jakob.scheid 53c7cb1c79 feat(language-switch-in-footer): use CSS variable for the language switch button vertical padding 2026-06-01 18:50:14 +02:00
jakob.scheid 368747f323 feat(language-switch-in-footer): add footer segment flexbox gap 2026-06-01 18:50:14 +02:00
jakob.scheid a46a4da005 feat(language-switch-in-footer): make footer segment class flexbox 2026-06-01 18:50:14 +02:00
jakob.scheid 5e15c01ed6 feat(language-switch-in-footer): add language switch button component to the footer 2026-06-01 18:50:14 +02:00
jakob.scheid c64539539d feat(language-switch-in-footer): remove language switch button component from the navbar 2026-06-01 18:50:14 +02:00
jakob.scheid fea440edaa Merge pull request 'Show search bar below the navbar on the search results view on small screens' (#99) from feature/place-search-bar-below-navbar into main
Deploy on dev / Deploy on dev (push) Successful in 35s
Reviewed-on: #99
Reviewed-by: Johannes D. Vos
2026-06-01 18:49:58 +02:00
jakob.scheid 47744e7be6 feat(place-search-bar-below-navbar): add horizontal padding to the search bar wrapper in the navbar (on large screens) 2026-06-01 17:49:27 +02:00
jakob.scheid 6a31373b7d feat(place-search-bar-below-navbar): set width of the search bar wrapper in the navbar (on large screens) 2026-06-01 17:48:50 +02:00
jakob.scheid a824ee57df feat(place-search-bar-below-navbar): add padding to the search bar wrapper below the navbar (on small screens) 2026-06-01 17:45:20 +02:00
jakob.scheid 9ed8790649 feat(place-search-bar-below-navbar): show search bar wrapper component below the navbar on small screens 2026-06-01 17:44:06 +02:00
jakob.scheid dd678fb393 set search form width to 100% 2026-06-01 17:39:11 +02:00
jakob.scheid 17570d400d feat(place-search-bar-below-navbar): hide navbar search bar wrapper component on small screens 2026-06-01 15:26:47 +02:00
jakob.scheid cabebfb9fe feat(place-search-bar-below-navbar): use navbar search bar wrapper component in the navbar 2026-06-01 15:23:57 +02:00
jakob.scheid 9b175e3923 feat(place-search-bar-below-navbar): add search bar model to the navbar search bar wrapper component 2026-06-01 15:23:35 +02:00
jakob.scheid 873bd74804 feat(place-search-bar-below-navbar): show search bar in the navbar search bar wrapper component only when the search results view is active 2026-06-01 15:20:30 +02:00
jakob.scheid 0df77ef6ee feat(place-search-bar-below-navbar): integrate search bar in the navbar search bar wrapper component 2026-06-01 15:17:32 +02:00
jakob.scheid 4daf550568 feat(place-search-bar-below-navbar): add empty navbar search bar wrapper component 2026-06-01 15:16:09 +02:00
jakob.scheid f7a76d5d20 Merge pull request 'Make the search bar on the start page fully wide on small screens' (#94) from feature/fully-wide-search-bar-on-startpage-on-small-screens into main
Deploy on dev / Deploy on dev (push) Successful in 35s
Reviewed-on: #94
Reviewed-by: Johannes D. Vos
2026-06-01 15:00:41 +02:00
jakob.scheid 34af4c874f add ARIA label to the search submit button 2026-06-01 14:34:11 +02:00
jakob.scheid c98e8d1f96 add title to the search submit button 2026-06-01 14:33:55 +02:00
jakob.scheid a2268ea275 adapt the color of the magnifying glass search icon to the color scheme 2026-06-01 14:23:05 +02:00
jakob.scheid 720fbd5697 support old browsers 2026-06-01 14:15:30 +02:00
jakob.scheid ba754771f4 Center magnifying glass icon on the search submit button 2026-06-01 13:45:53 +02:00
jakob.scheid b0bf852e26 replace 'search' text on the search submit button with a magnifying glass icon 2026-06-01 13:44:03 +02:00
jakob.scheid 83351110b3 add magnifying glass icon 2026-06-01 13:35:48 +02:00
jakob.scheid a30703dc3e feat(fully-wide-search-bar): Reduce main content horizontal padding on small screens 2026-06-01 13:29:36 +02:00
jakob.scheid f480794817 Merge pull request 'Use icons instead of Unicode characters' (#91) from feature/icons into main
Deploy on dev / Deploy on dev (push) Successful in 33s
Reviewed-on: #91
Reviewed-by: Johannes D. Vos
2026-06-01 12:28:35 +02:00
jakob.scheid 0e9e0ecec7 feat(icons): use icon at the language switching button 2026-06-01 12:28:20 +02:00
jakob.scheid 54090751a5 feat(icons): use icons at the color scheme button 2026-06-01 12:28:20 +02:00
jakob.scheid 274d25d654 feat(icons): Invert icons according to the color scheme 2026-06-01 12:28:20 +02:00
jakob.scheid 35db1d65f2 Add --invert CSS variable for the invertion (according to the color scheme) 2026-06-01 12:28:20 +02:00
jakob.scheid 20e350d88b Add icon component <img> flexbox wrapper 2026-06-01 12:28:20 +02:00
jakob.scheid 534734ce9a Ensure unit of the size prop in the icon component 2026-06-01 12:28:20 +02:00
jakob.scheid 1679800272 Add utility function to ensure that a CSS dimension has a unit 2026-06-01 12:28:20 +02:00
jakob.scheid bf3786249b Add icon component prop to specify the size of the icon 2026-06-01 12:28:20 +02:00
jakob.scheid f24749423b Add icon component 2026-06-01 12:28:20 +02:00
jakob.scheid b617078e58 Add black and white circle icon 2026-06-01 12:28:20 +02:00
jakob.scheid c5467620c5 Add sun icon 2026-06-01 12:28:20 +02:00
jakob.scheid fc4584f932 Add crescent moon icon 2026-06-01 12:28:20 +02:00
jakob.scheid da319c28d5 Add chevron down icon 2026-06-01 12:28:20 +02:00
jakob.scheid 6f2aaa62f0 Merge pull request 'Add changelog' (#92) from docs/changelog into main
Deploy on dev / Deploy on dev (push) Successful in 35s
Reviewed-on: #92
Reviewed-by: Johannes D. Vos
2026-06-01 12:28:05 +02:00
jakob.scheid 8f65d3ae60 docs(changelog): add 'Added' section to the unreleased release 2026-06-01 12:27:51 +02:00
jakob.scheid f6d848a714 docs(changelog): add changelog boilerplate 2026-06-01 12:27:51 +02:00
jakob.scheid 2340a6a193 Merge pull request 'Set up Vitest' (#93) from testing/set-up-vitest into main
Deploy on dev / Deploy on dev (push) Successful in 35s
Reviewed-on: #93
Reviewed-by: Jakob Gregory
2026-06-01 12:26:52 +02:00
jakob.scheid 0dcb6f0821 chore(set-up-vitest): Add testing configuration 2026-06-01 01:01:38 +02:00
jakob.scheid f0dc5d4bdc chore(set-up-vitest): add jsdom dependency 2026-06-01 00:58:14 +02:00
jakob.scheid 59e19c7666 chore(set-up-vitest): add test scripts 2026-06-01 00:55:02 +02:00
jakob.scheid 0373ea20f7 chore(set-up-vitest): add Vitest dependency 2026-06-01 00:53:31 +02:00
41 changed files with 3367 additions and 234 deletions
+1071 -2
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
# Changelog
## [Unreleased]
### Added
- Start page
- Settings page
- Footer
- Navbar
- Searchbar
- Icons
- Internationalization and some major languages
- Color scheme
- Search results view with error message which indicates that the search is not available
+836 -1
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -6,7 +6,9 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"test": "vitest",
"test:run": "vitest run"
}, },
"dependencies": { "dependencies": {
"terser": "^5.47.1", "terser": "^5.47.1",
@@ -15,8 +17,11 @@
"vue-router": "^5.0.6" "vue-router": "^5.0.6"
}, },
"devDependencies": { "devDependencies": {
"@vue/test-utils": "^2.4.6",
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.6",
"jsdom": "^29.1.1",
"vite": "^8.0.10", "vite": "^8.0.10",
"vite-plugin-html": "^3.2.2" "vite-plugin-html": "^3.2.2",
"vitest": "^4.1.7"
} }
} }
+11 -2
View File
@@ -42,13 +42,22 @@ watch(colorScheme, val => updateColorScheme(val))
</template> </template>
<style scoped> <style scoped>
.main-content { #app-wrapper {
--main-content-padding-x: 30px; --main-content-padding-x: 30px;
--main-content-padding-y: 40px; --main-content-padding-y: 40px;
--main-content-padding: var(--main-content-padding-y) var(--main-content-padding-x) 0; --main-content-padding: var(--main-content-padding-y) var(--main-content-padding-x);
}
.main-content {
flex-grow: 1; flex-grow: 1;
} }
@media (max-width: 48rem) {
#app-wrapper {
--main-content-padding-x: 15px;
}
}
#app-wrapper { #app-wrapper {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
@@ -1,4 +1,4 @@
/* <!--
Copyright 2026 Seekra Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@@ -12,4 +12,15 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
stroke="currentColor"
stroke-width="3"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
d="M4 8 l8 8 l8 -8"
/>
</svg>

Before

Width:  |  Height:  |  Size: 552 B

After

Width:  |  Height:  |  Size: 830 B

+38
View File
@@ -0,0 +1,38 @@
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
fill="currentColor"
d="
M12 2
a10 10 0 0 0 -10 10
a10 10 0 0 0 10 10
a10 10 0 0 0 10 -10
a10 10 0 0 0 -10 -10
"
/>
<path
style="filter: invert(1)"
fill="currentColor"
d="
M12 3
v18
a9 9 0 0 0 9 -9
a9 9 0 0 0 -9 -9
"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+36
View File
@@ -0,0 +1,36 @@
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
fill="currentColor"
stroke="none"
stroke-width="0"
d="
M12.621094 23.988281
c-6.960938 0 -12.621094 -5.664062000000001 -12.621094 -12.621093
c0 -4.8203130000000005 2.683594 -9.148438 7.003906 -11.300781800000001
c0.19140600000000063 -0.09765619999999942 0.4257819999999999 -0.058593699999999416 0.578125 0.09374980000000058
c0.15234400000000026 0.152344 0.19531300000000051 0.38671900000000003 0.09765699999999988 0.582032
c-0.7890629999999996 1.601562 -1.1875 3.320312 -1.1875 5.113281000000001
c0.0 6.402342999999999 5.207031 11.613281 11.609374000000003 11.613281
c1.8125 0.0 3.550781999999998 -0.41015600000000063 5.167968999999999 -1.2148439999999994
c0.19531299999999874 -0.09765600000000063 0.42968799999999874 -0.05859399999999937 0.5820310000000006 0.09375
c0.15234399999999937 0.15234399999999937 0.1914069999999981 0.38671899999999937 0.0976569999999981 0.5820319999999981
c-2.140625 4.351562000000001 -6.480468999999999 7.058593000000002 -11.328125 7.058593000000002
"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+36
View File
@@ -0,0 +1,36 @@
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
stroke="currentColor"
stroke-width="3"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
d="M2 22 l8 -8"
/>
<circle
stroke="currentColor"
stroke-width="3"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
cx="15.5"
cy="8.5"
r="6.5"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+45
View File
@@ -0,0 +1,45 @@
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<circle fill="currentColor" r="6" cx="12" cy="12" />
<!-- sunbeams -->
<!-- left -->
<circle fill="currentColor" r="2" cx="3" cy="12" />
<!-- right -->
<circle fill="currentColor" r="2" cx="21" cy="12" />
<!-- top -->
<circle fill="currentColor" r="2" cx="12" cy="3" />
<!-- top -->
<circle fill="currentColor" r="2" cx="12" cy="21" />
<!-- top left -->
<circle fill="currentColor" r="2" cx="5.636038969321072" cy="5.636038969321072" />
<!-- top right -->
<circle fill="currentColor" r="2" cx="18.36396103067893" cy="5.636038969321072" />
<!-- bottom left -->
<circle fill="currentColor" r="2" cx="5.636038969321072" cy="18.36396103067893" />
<!-- bottom right -->
<circle fill="currentColor" r="2" cx="18.36396103067893" cy="18.36396103067893" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -15,6 +15,8 @@ limitations under the License.
--> -->
<script setup> <script setup>
import Icon from '@/features/icons/components/Icon.vue';
import { inject } from 'vue'; import { inject } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -29,9 +31,9 @@ const colorSchemeNextMapper = {
}; };
const colorSchemeIconMapper = { const colorSchemeIconMapper = {
'dark': '', 'dark': 'crescent-moon',
'light': '', 'light': 'sun',
'auto': '' 'auto': 'circle-black-white'
}; };
const getTooltipTranslation = function (colorScheme) { const getTooltipTranslation = function (colorScheme) {
@@ -45,7 +47,10 @@ const getTooltipTranslation = function (colorScheme) {
:aria-label="getTooltipTranslation(colorScheme)" :aria-label="getTooltipTranslation(colorScheme)"
:title="getTooltipTranslation(colorScheme)" :title="getTooltipTranslation(colorScheme)"
> >
{{ colorSchemeIconMapper[colorSchemeNextMapper[colorScheme]] }} <Icon
:name="colorSchemeIconMapper[colorSchemeNextMapper[colorScheme]]"
size="16"
/>
</button> </button>
</template> </template>
+7 -8
View File
@@ -15,6 +15,8 @@ limitations under the License.
--> -->
<script setup> <script setup>
import LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
@@ -36,11 +38,10 @@ const copyrightPeriod =
<RouterLink to="settings" class="link"> <RouterLink to="settings" class="link">
{{ t('preferences.settings') }} {{ t('preferences.settings') }}
</RouterLink> </RouterLink>
<LanguageSwitchButton />
</div> </div>
<div class="footer-segment"> <div class="footer-segment">
<div class="copyright-note"> &copy; {{ copyrightPeriod }} Seekra
&copy; {{ copyrightPeriod }} Seekra
</div>
</div> </div>
</footer> </footer>
</template> </template>
@@ -51,6 +52,9 @@ const copyrightPeriod =
} }
.footer-segment { .footer-segment {
display: flex;
justify-content: center;
gap: 32px;
padding: var(--padding-y); padding: var(--padding-y);
background-color: var(--light-bg); background-color: var(--light-bg);
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
@@ -59,9 +63,4 @@ const copyrightPeriod =
.global-footer a { .global-footer a {
color: var(--dark); color: var(--dark);
} }
.copyright-note {
display: flex;
justify-content: center;
}
</style> </style>
@@ -15,6 +15,8 @@ limitations under the License.
--> -->
<script setup> <script setup>
import Icon from '@/features/icons/components/Icon.vue';
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { loadLanguage, LANGUAGES_RTL, SUPPORTED_LANGUAGES } from '@/i18n'; import { loadLanguage, LANGUAGES_RTL, SUPPORTED_LANGUAGES } from '@/i18n';
@@ -64,10 +66,20 @@ const open = function () {
aria-haspopup="listbox" aria-haspopup="listbox"
> >
<span class="lang-code">{{ t(`preferences.locale.languages.${locale}`) }}</span> <span class="lang-code">{{ t(`preferences.locale.languages.${locale}`) }}</span>
<span class="chevron" :class="{ open: isOpen }"></span> <span class="chevron" :class="{ open: isOpen }">
<Icon name="chevron-down" size="1em" />
</span>
</button> </button>
<ul v-if="isOpen" ref="languageDropdown" class="language-dropdown" role="listbox"> <ul
v-if="isOpen"
ref="languageDropdown"
class="language-dropdown"
role="listbox"
:style="{
bottom: 'calc(var(--offset) + 2 * var(--trigger-padding-y) + 1em)' // easier to add auto adaption to the available space
}"
>
<li <li
v-for="lang in SUPPORTED_LANGUAGES" v-for="lang in SUPPORTED_LANGUAGES"
:key="lang" :key="lang"
@@ -84,6 +96,7 @@ const open = function () {
<style scoped> <style scoped>
.language-switch { .language-switch {
--trigger-padding-y: 4px;
position: relative; position: relative;
} }
@@ -94,7 +107,7 @@ const open = function () {
background: none; background: none;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
padding: 4px 10px; padding: var(--trigger-padding-y) 10px;
cursor: pointer; cursor: pointer;
color: var(--dark); color: var(--dark);
} }
@@ -114,9 +127,9 @@ const open = function () {
} }
.language-dropdown { .language-dropdown {
--offset: 6px;
position: absolute; position: absolute;
right: 0; right: 0;
top: calc(100% + 6px);
background-color: var(--light-bg); background-color: var(--light-bg);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
@@ -0,0 +1,114 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { expect, test, describe, vi, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import LanguageSwitchButton from '../LanguageSwitchButton.vue';
import { loadLanguage } from '@/i18n';
vi.mock('@/i18n', () => ({
loadLanguage: vi.fn(() => Promise.resolve()),
LANGUAGES_RTL: ['ar', 'he'],
SUPPORTED_LANGUAGES: ['en', 'de', 'ar']
}));
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,
locale: { value: 'de' }
})
}));
vi.mock('@/features/icons/components/Icon.vue', () => ({
default: {
name: 'Icon',
template: '<span>Icon</span>'
}
}));
describe('LanguageSwitchButton.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.lang = '';
document.documentElement.dir = '';
});
test('renders correctly with initial state closed', () => {
const wrapper = mount(LanguageSwitchButton);
expect(wrapper.find('.language-button').exists()).toBe(true);
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
expect(wrapper.find('.language-button').attributes('aria-expanded')).toBe('false');
});
test('opens the dropdown when language button is clicked', async () => {
const wrapper = mount(LanguageSwitchButton);
const button = wrapper.find('.language-button');
await button.trigger('click');
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
expect(button.attributes('aria-expanded')).toBe('true');
});
const languageTestCases = [
{ code: 'en', expectedDir: 'ltr' },
{ code: 'de', expectedDir: 'ltr' },
{ code: 'ar', expectedDir: 'rtl' }
];
test.for(languageTestCases)('selectLanguage($code) sets localStorage, html attributes and changes layout direction to $expectedDir', async ({ code, expectedDir }) => {
const wrapper = mount(LanguageSwitchButton);
await wrapper.find('.language-button').trigger('click');
const options = wrapper.findAll('.language-dropdown li');
const optionToClick = options.find(opt => opt.text().includes(code));
await optionToClick.trigger('click');
expect(loadLanguage).toHaveBeenCalledWith(code);
expect(localStorage.getItem('locale')).toBe(code);
expect(document.documentElement.lang).toBe(code);
expect(document.documentElement.dir).toBe(expectedDir);
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
});
test('closes the dropdown when clicking outside the component', async () => {
const wrapper = mount(LanguageSwitchButton, {
attachTo: document.body
});
await wrapper.find('.language-button').trigger('click');
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
await new Promise(resolve => setTimeout(resolve, 0));
const externalDiv = document.createElement('div');
document.body.appendChild(externalDiv);
const clickEvent = new MouseEvent('click', { bubbles: true });
externalDiv.dispatchEvent(clickEvent);
await wrapper.vm.$nextTick();
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
wrapper.unmount();
externalDiv.remove();
});
});
+54
View File
@@ -0,0 +1,54 @@
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script setup>
import { computed } from 'vue';
import { ensureUnit } from '@/utils/cssDimensions';
const props = defineProps({
name: {
required: true,
type: String
},
size: {
type: [Number, String],
default: 24
}
})
const icons = import.meta.glob('@/assets/icons/*.svg', {
eager: true,
import: 'default'
})
const Icon = computed(() => icons[`/src/assets/icons/${props.name}.svg`])
const size = computed(() => ensureUnit(props.size))
</script>
<template>
<div class="icon-container">
<img :src="Icon" :style="{ height: `${size}`, width: `${size}` }" />
</div>
</template>
<style scoped>
.icon-container {
display: flex;
align-items: center;
filter: var(--invert);
}
</style>
@@ -1,69 +0,0 @@
```vue
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
// 1. ALLE Sprachen importieren (Verhindert den ReferenceError)
import de from '@/legal/privacy/de.md?raw';
import en from '@/legal/privacy/en.md?raw';
import fr from '@/legal/privacy/fr.md?raw';
import es from '@/legal/privacy/es.md?raw';
import it from '@/legal/privacy/it.md?raw';
import pt from '@/legal/privacy/pt.md?raw';
const { locale } = useI18n();
const content = computed(() => {
const map = {
de,
en,
fr,
es,
it,
pt
};
// Falls eine Sprache mal nicht existiert, nutzen wir 'de' oder 'en' als Fallback
return map[locale.value] || de;
});
</script>
<template>
<main class="privacy-policy-content main-content-padding">
<h1>{{ $t('legal.privacy.title') }}</h1>
<div class="markdown-body">{{ content }}</div>
</main>
</template>
<style scoped>
.privacy-policy-content {
max-width: 900px;
margin: 0 auto;
padding-top: 40px;
padding-bottom: 40px;
}
/* Sorgt dafür, dass die Zeilenumbrüche aus den .md Dateien erhalten bleiben */
.markdown-body {
white-space: pre-wrap;
font-family: inherit;
line-height: 1.6;
}
</style>
+28 -29
View File
@@ -15,25 +15,9 @@ limitations under the License.
--> -->
<script setup> <script setup>
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue'; import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
import LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
import logo from '@/assets/images/logo.svg'; import logo from '@/assets/images/logo.svg';
import Searchbar from '@/features/search/components/Searchbar.vue'; import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
const route = useRoute();
const searchQueryModel = defineModel();
watch(() => route.name, name => {
searchQueryModel.value = name === 'searchResults' ? route.query.q || '' : '';
});
watch(() => route.query.q, q => {
if (route.name === 'searchResults') {
searchQueryModel.value = q || '';
}
});
</script> </script>
<template> <template>
@@ -41,21 +25,16 @@ watch(() => route.query.q, q => {
<RouterLink to="/" class="link button link"> <RouterLink to="/" class="link button link">
<img :src="logo" alt="Seekra" class="nav-logo" /> <img :src="logo" alt="Seekra" class="nav-logo" />
</RouterLink> </RouterLink>
<Searchbar <NavbarSearchBarWrapper class="navbar-search-bar-wrapper" />
v-if="route.name === 'searchResults'"
class="search-bar"
v-model="searchQueryModel"
auto-submit
/>
<ul class="right-links"> <ul class="right-links">
<li>
<LanguageSwitchButton />
</li>
<li> <li>
<ColorSchemeButton /> <ColorSchemeButton />
</li> </li>
</ul> </ul>
</nav> </nav>
<div class="navbar-search-bar-wrapper-small-screens-wrapper">
<NavbarSearchBarWrapper class="navbar-search-bar-wrapper-small-screens" />
</div>
</template> </template>
<style scoped> <style scoped>
@@ -63,7 +42,7 @@ watch(() => route.query.q, q => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 18px 40px; padding: 18px var(--main-content-padding-x);
height: 42px; height: 42px;
} }
@@ -89,7 +68,27 @@ watch(() => route.query.q, q => {
height: 24px; height: 24px;
width: auto; width: auto;
} }
.search-bar {
width: 70%; .navbar-search-bar-wrapper {
width: 100%;
padding: 0 30px;
}
.navbar-search-bar-wrapper-small-screens-wrapper {
padding: 0 15px 15px;
}
.navbar-search-bar-wrapper-small-screens {
display: none;
}
@media (max-width: 48rem) {
.navbar-search-bar-wrapper {
display: none;
}
.navbar-search-bar-wrapper-small-screens {
display: block;
}
} }
</style> </style>
@@ -0,0 +1,43 @@
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script setup>
import Searchbar from '@/features/search/components/Searchbar.vue';
import { watch } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const searchQueryModel = defineModel();
watch(() => route.name, name => {
searchQueryModel.value = name === 'searchResults' ? route.query.q || '' : '';
});
watch(() => route.query.q, q => {
if (route.name === 'searchResults') {
searchQueryModel.value = q || '';
}
});
</script>
<template>
<Searchbar
v-if="route.name === 'searchResults'"
v-model="searchQueryModel"
auto-submit
/>
</template>
+26 -3
View File
@@ -15,6 +15,8 @@ limitations under the License.
--> -->
<script setup> <script setup>
import Icon from '@/features/icons/components/Icon.vue';
const searchQuery = defineModel(); const searchQuery = defineModel();
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -37,7 +39,7 @@ const submitSearch = function () {
<template> <template>
<div> <div>
<form @submit.prevent="submitSearch"> <form @submit.prevent="submitSearch" class="search-form">
<div class="search-wrapper"> <div class="search-wrapper">
<input <input
v-model="searchQuery" v-model="searchQuery"
@@ -45,7 +47,14 @@ const submitSearch = function () {
:placeholder="t('search.searchBar.placeholder')" :placeholder="t('search.searchBar.placeholder')"
required required
/> />
<button type="submit" class="search-button">{{ t('search.searchBar.submit') }}</button> <button
type="submit"
class="search-button"
:title="t('search.searchBar.submit')"
:aria-label="t('search.searchBar.submit')"
>
<Icon class="search-icon" name="magnifying-glass" size="1.1em" />
</button>
</div> </div>
</form> </form>
</div> </div>
@@ -64,7 +73,8 @@ const submitSearch = function () {
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y) + var(--padding)); border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y) + var(--padding));
padding: var(--padding); padding: var(--padding);
padding-left: var(--padding-left); padding-left: var(--padding-left);
width: calc(100% - var(--padding-left)); width: 100%;
box-sizing: border-box;
} }
.search-wrapper input { .search-wrapper input {
@@ -74,11 +84,13 @@ const submitSearch = function () {
font-size: 1rem; font-size: 1rem;
background: transparent; background: transparent;
height: calc(var(--content-height) + 2 * var(--submit-button-padding-y)); height: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
padding-left: 12px;
} }
.search-button { .search-button {
font-size: 1rem; font-size: 1rem;
height: calc(var(--content-height) + 2 * var(--submit-button-padding-y)); height: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
width: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y)); border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y));
border: none; border: none;
padding: var(--submit-button-padding-y) 20px; padding: var(--submit-button-padding-y) 20px;
@@ -86,9 +98,20 @@ const submitSearch = function () {
color: var(--white); color: var(--white);
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
display: flex;
justify-content: center;
align-items: center;
}
.search-button .search-icon {
filter: invert(1);
} }
.search-button:hover { .search-button:hover {
background: var(--primary-color-l-1); background: var(--primary-color-l-1);
} }
.search-form {
width: 100%;
}
</style> </style>
@@ -53,9 +53,7 @@ const { t } = useI18n();
--error-message-padding: 2em; --error-message-padding: 2em;
display: flex; display: flex;
justify-content: center; justify-content: center;
width: calc(100% - 2 * var(--main-content-padding-x)); width: 100%;
position: absolute;
top: calc(50vh - 0.5 * var(--error-message-height) - var(--error-message-padding));
} }
.search-results-error-message { .search-results-error-message {
@@ -0,0 +1,699 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { describe, test, expect } from 'vitest';
import { validateSettingsConfig, validateEntry, validateSelectionOptions, assertType, assertString } from '../settingsValidator';
describe('validateSettingsConfig', () => {
test.for([
{ raw: false, expected: false },
{ raw: true, expected: false },
{ raw: 0, expected: false },
{ raw: 42, expected: false },
{ raw: '', expected: false },
{ raw: ' ', expected: false },
{ raw: 'a', expected: false },
{ raw: {}, expected: false },
{ raw: {
contents: []
}, expected: true },
{ raw: {
contents: [
{
type: 'section',
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
}
]
}, expected: true },
{ raw: {
contents: [
{
type: 'section',
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
]
}, expected: true },
{ raw: {
contents: [
{
type: 'section',
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'aSection',
i18n: 'settings.settings.aSection',
content: [
{
type: 'bool',
name: 'Enable feature 43',
i18n: 'settings.settings.aSection.enableFeature43'
},
{
type: 'selection',
name: 'language',
i18n: 'settings.settings.aSection.language.label',
default: 'en',
options: [
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
]
},
{
type: 'section',
name: 'section2',
i18n: 'settings.settings.aSection.section2.label',
content: [
{
type: 'string',
name: 'string',
i18n: 'settings.settings.aSection.sections.string',
default: 'str'
}
]
},
]
},
]
}, expected: true },
{ raw: {
contents: [
{
type: 'section',
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'aSection',
i18n: 'settings.settings.aSection',
content: [
{
type: 'bool',
name: 'Enable feature 43',
i18n: 'settings.settings.aSection.enableFeature43'
},
{
type: 'selection',
name: 'language',
i18n: 'settings.settings.aSection.language.label',
default: 'en',
allowMultiple: false,
options: [
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
]
},
{
type: 'section',
name: 'section2',
i18n: 'settings.settings.aSection.section2.label',
content: [
{
type: 'string',
name: 'string',
i18n: 'settings.settings.aSection.sections.string',
default: 'str'
}
]
},
]
},
]
}, expected: true },
{ raw: {
contents: [
{
type: 'section',
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'aSection',
i18n: 'settings.settings.aSection',
content: [
{
type: 'bool',
name: 'Enable feature 43',
i18n: 'settings.settings.aSection.enableFeature43',
default: 'true'
},
{
type: 'selection',
name: 'language',
i18n: 'settings.settings.aSection.language.label',
default: 'en',
options: [
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
]
},
{
type: 'section',
name: 'section2',
i18n: 'settings.settings.aSection.section2.label',
content: [
{
type: 'string',
name: 'string',
i18n: 'settings.settings.aSection.sections.string',
default: 42
}
]
},
]
},
]
}, expected: false },
{ raw: {
content: [
{
type: 'section',
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'aSection',
i18n: 'settings.settings.aSection',
content: [
{
type: 'bool',
name: 'Enable feature 43',
i18n: 'settings.settings.aSection.enableFeature43'
},
{
type: 'selection',
name: 'language',
i18n: 'settings.settings.aSection.language.label',
default: 'en',
options: [
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
]
},
{
type: 'section',
name: 'section2',
i18n: 'settings.settings.aSection.section2.label',
content: [
{
type: 'string',
name: 'string',
i18n: 'settings.settings.aSection.sections.string',
default: 'str'
}
]
},
]
},
]
}, expected: false },
{ raw: {
content: [
{
type: 'sectio',
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'aSection',
i18n: 'settings.settings.aSection',
content: [
{
type: 'bool',
name: 'Enable feature 43',
i18n: 'settings.settings.aSection.enableFeature43'
},
{
type: 'selection',
name: 'language',
i18n: 'settings.settings.aSection.language.label',
default: 'en',
options: [
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
]
},
{
type: 'section',
name: 'section2',
i18n: 'settings.settings.aSection.section2.label',
content: [
{
type: 'string',
name: 'string',
i18n: 'settings.settings.aSection.sections.string',
default: 'str'
}
]
},
]
},
]
}, expected: false },
{ raw: {
content: [
{
type: 'sectio',
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
type: 'section',
name: 'aSection',
i18n: 'settings.settings.aSection',
content: [
{
type: 'bool',
name: 'Enable feature 43',
i18n: 'settings.settings.aSection.enableFeature43'
},
{
type: 'selection',
name: 'language',
i18n: 'settings.settings.aSection.language.label',
default: 'en',
allowMultiple: 'false',
options: [
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
]
},
{
type: 'section',
name: 'section2',
i18n: 'settings.settings.aSection.section2.label',
content: [
{
type: 'string',
name: 'string',
i18n: 'settings.settings.aSection.sections.string',
default: 'str'
}
]
},
]
},
]
}, expected: false }
])('returns valid: $expected', ({ raw, expected }) => {
const result = validateSettingsConfig(raw);
if (!result.valid) {
console.error('Error message:', result.error);
};
expect(result.valid).toBe(expected);
});
});
describe('validateEntry', () => {
test.for([
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: false }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 42 }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 0 }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: -42.7 }],
[{ name: 'aString', type: 'string', i18n: 'aString.label' }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: '' }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: 'Seekra is great!' }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', options: [
{ name: 'yes', i18n: 'selectSomething.options.yes' },
{ name: 'no', i18n: 'selectSomething.options.no' },
{ name: 'maybe', i18n: 'selectSomething.options.maybe' }
] }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'no', options: [
{ name: 'yes', i18n: 'selectSomething.options.yes' },
{ name: 'no', i18n: 'selectSomething.options.no' },
{ name: 'maybe', i18n: 'selectSomething.options.maybe' }
] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true }
] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.43.enable', default: true }
] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.43.enable', default: true },
{ name: 'aSecondSection', type: 'section', i18n: 'sections.aSecondSection.title', content: [
{ name: 'enableFeature44', type: 'bool', i18n: 'feature.44.enable', default: true },
] }
] }]
])('throws no error for the entry %s', ([ entry ]) => {
expect(() => validateEntry(entry)).not.throws(Error);
});
test.for([
[{ name: 'enableFeature42', type: 'bool', i18n: '' }],
[{ name: 'enableFeature42', type: 'thisTypeDoesNotExistAndHasAVeryLongName', i18n: 'feature.42.enable' }],
[{ name: '', type: 'thisTypeDoesNotExistAndHasAVeryLongName', i18n: 'feature.42.enable' }],
[{ name: '', type: 'thisTypeDoesNotExistAndHasAVeryLongName', i18n: '' }],
[{ name: '', type: 'bool', i18n: '' }],
[{ name: '', type: 'bool', i18n: '' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 42 }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: '42' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'Seekra' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'true' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'false' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'undefined' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'null' }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: [] }],
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: {} }],
[{ name: 'enableFeature42', type: '', i18n: 'feature.42.enable', default: {} }],
[{ name: '', type: 'number', i18n: 'aNumber.label' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: '42' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 'zero' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 'undefined' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 'false' }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: true }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: false }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: [] }],
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: {} }],
[{ name: 'aNumber', type: '', i18n: 'aNumber.label', default: {} }],
[{ name: 'aNumber', type: '', i18n: 'aNumber.label', default: 42 }],
[{ name: 'aString', type: 'string', i18n: '' }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: 42 }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: true }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: false }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: [] }],
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: {} }],
[{ name: 'aString', type: 'string', i18n: '', default: {} }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', options: true }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', options: [] }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'c', options: [
{ name: 'a', i18n: 'a' },
{ name: 'b', i18n: 'b' }
] }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', options: [
{ name: 'a', i18n: 'a' },
{ name: 'b', i18n: '' },
{ name: 'c' }
] }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', allowMultiple: 'false', options: [
{ name: 'a', i18n: 'a' },
{ name: 'b', i18n: '' },
{ name: 'c' }
] }],
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'no', options: [
{ name: 'yes', i18n: '' },
{ name: '', i18n: 'selectSomething.options.no' },
{ name: 'maybe', i18n: 'selectSomething.options.maybe' },
{ name: '', i18n: '' },
{ name: 42, i18n: 43 },
{ name: '' },
{ i18n: '' },
{}
] }],
[{ name: 'aSection', type: 'section', i18n: '', content: [] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: '' }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: '[]' }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: 'a' }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.42.enable', default: 0 },
{ name: 'enableFeature44', type: 'bool' }
] }],
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.43.enable', default: true },
{ name: 'aSecondSection', type: 'section', i18n: 'sections.aSecondSection.title', content: 'Hello' },
{ name: 'aSecondSection', type: 'section', i18n: '', content: [] },
{ name: 'aSecondSection', type: 'section', i18n: 'i18n', content: [] },
{ name: 'aSecondSection', type: 'section', content: [] }
] }]
])('throws an error for the entry %s', ([ entry ]) => {
expect(() => validateEntry(entry)).throws(Error);
});
});
describe('validateSelectionOptions', () => {
test.for([
[[{ name: 'test', i18n: 'test.label' }]],
[[{ name: 'test', i18n: 'test.label' }, { name: 'test2', i18n: 'test2.label' }]],
[[{ name: 'test', i18n: 'test.label' }, { name: 'test', i18n: 'test2.label' }, { name: 'test3', i18n: 'test.label' }]]
])('throws no error for the options %s', ([ options ]) => {
expect(() => validateSelectionOptions(options)).not.throws(Error);
});
test.for([
[[{}]],
[[{ i18n: '' }]],
[[{ i18n: 'a' }]],
[[{ name: 'a' }]],
[[{ name: '' }]],
[[{ name: '', i18n: 'test.label' }]],
[[{ name: 'test', i18n: '' }]],
[[{ name: '', i18n: '' }]],
[[{ name: '', i18n: 'test.label' }, { name: 'test2', i18n: '' }]],
[[{ name: 'test', i18n: '' }, { name: '', i18n: 'test2.label' }, { name: '', i18n: ' ' }, { name: '42', i18n: '42.i18n' }]],
[[]]
])('throws an error for the options %s', ([ options ]) => {
expect(() => validateSelectionOptions(options)).throws(Error);
});
});
describe('assertType', () => {
test.for([
['bool'],
['number'],
['string'],
['selection'],
['section']
])('throws no error for the value %s', ([ value ]) => {
expect(() => assertType(value)).not.throw(Error);
});
test.for([
[''],
[' '],
[' '],
[' '],
['42'],
['0'],
['-42'],
['-42.0'],
['-0.0'],
['a'],
['ab'],
['SeekraIsGreat!'],
['Seekra is great!'],
[undefined],
[null]
])('throws an error for the value %s', ([ value ]) => {
expect(() => assertType(value)).throw(Error);
});
});
describe('assertString', () => {
test.for([
['a'],
['b'],
['ab'],
['0'],
['42'],
['null'],
['undefined'],
['()&%())']
])('throws no error for the value %s', ([ value ]) => {
expect(() => assertString(value)).not.throw(Error);
});
test.for([
[0],
[1],
[42],
[-1],
[-42],
[''],
[' '],
[' '],
[' ']
])('throws an error for the value %s', ([ value ]) => {
expect(() => assertString(value)).throws(Error);
});
});
@@ -16,13 +16,13 @@ limitations under the License.
const VALID_TYPES = ['bool', 'number', 'string', 'selection', 'section']; const VALID_TYPES = ['bool', 'number', 'string', 'selection', 'section'];
function assertString(value, path) { export const assertString = function assertString (value, path) {
if (typeof value !== 'string' || value.trim() === '') { if (typeof value !== 'string' || value.trim() === '') {
throw new Error(`[settings] "${path}" must be a non-empty string`); throw new Error(`[settings] "${path}" must be a non-empty string`);
} }
} }
function assertType(value, path) { export const assertType = function assertType (value, path) {
if (!VALID_TYPES.includes(value)) { if (!VALID_TYPES.includes(value)) {
throw new Error( throw new Error(
`[settings] "${path}" has invalid type "${value}". Must be one of: ${VALID_TYPES.join(', ')}` `[settings] "${path}" has invalid type "${value}". Must be one of: ${VALID_TYPES.join(', ')}`
@@ -30,21 +30,23 @@ function assertType(value, path) {
} }
} }
function validateSelectionOptions(options, path) { export const validateSelectionOptions = function validateSelectionOptions (options, path) {
if (!Array.isArray(options) || options.length === 0) { if (!Array.isArray(options) || options.length === 0) {
throw new Error(`[settings] "${path}.options" must be a non-empty array`); throw new Error(`[settings] "${path}.options" must be a non-empty array`);
} };
options.forEach((opt, i) => { options.forEach((opt, i) => {
assertString(opt.name, `${path}.options[${i}].name`); assertString(opt.name, `${path}.options[${i}].name`);
assertString(opt.i18n, `${path}.options[${i}].i18n`); assertString(opt.i18n, `${path}.options[${i}].i18n`);
}); });
} }
function validateEntry(entry, path) { export const validateEntry = function validateEntry (entry, path) {
assertType(entry.type, `${path}.type`); assertType(entry.type, `${path}.type`);
assertString(entry.name, `${path}.name`);
assertString(entry.i18n, `${path}.i18n`);
if (entry.type === 'section') { if (entry.type === 'section') {
assertString(entry.name, `${path}.name`);
if (!Array.isArray(entry.content)) { if (!Array.isArray(entry.content)) {
throw new Error(`[settings] "${path}.content" must be an array`); throw new Error(`[settings] "${path}.content" must be an array`);
} }
@@ -53,26 +55,31 @@ function validateEntry(entry, path) {
); );
return; return;
} }
assertString(entry.name, `${path}.name`); if (entry.type === 'selection') {
assertString(entry.i18n, `${path}.i18n`); validateSelectionOptions(entry.options, path);
if (typeof entry.allowMultiple !== 'boolean' && entry.allowMultiple) {
throw new Error(`[settings] "${path}.allowMultiple" must be a boolean`);
}
};
if (entry.default !== undefined) { if (entry.default !== undefined) {
if (entry.type === 'bool' && typeof entry.default !== 'boolean') { if (entry.type === 'bool' && typeof entry.default !== 'boolean') {
throw new Error(`[settings] "${path}.default" must be a boolean`); throw new Error(`[settings] "${path}.default" must be a boolean`);
} }
if (entry.type === 'number' && typeof entry.default !== 'number') { if (entry.type === 'number' && typeof entry.default !== 'number') {
throw new Error(`[settings] "${path}.default" must be a number`); throw new Error(`[settings] "${path}.default" must be a number`);
} }
if (entry.type === 'string' && typeof entry.default !== 'string') { if (entry.type === 'string' && typeof entry.default !== 'string') {
throw new Error(`[settings] "${path}.default" must be a string`); throw new Error(`[settings] "${path}.default" must be a string`);
} }
if (entry.type === 'selection') { if (entry.type === 'selection') {
validateSelectionOptions(entry.options, path); if (typeof entry.default !== 'string') {
if (typeof entry.allowMultiple !== 'boolean') { throw new Error(`[settings] "${path}.default" must be a string`);
throw new Error(`[settings] "${path}.allowMultiple" must be a boolean`); };
} if (!entry.options.map((option) => option.name).includes(entry.default)) {
} throw new Error(`[settings] option "${path}.default" does not exist`);
};
};
} }
} }
-3
View File
@@ -1,3 +0,0 @@
# Datenschutzerklärung
Hier steht deine Datenschutzerklärung auf Deutsch.
-3
View File
@@ -1,3 +0,0 @@
# Privacy Policy
Here is your privacy policy in English.
-3
View File
@@ -1,3 +0,0 @@
# Política de privacidad
Aquí tienes tu política de privacidad en español.
-3
View File
@@ -1,3 +0,0 @@
# Politique de confidentialité
Voici ta politique de confidentialité en français.
-3
View File
@@ -1,3 +0,0 @@
# Informativa sulla privacy
Qui trovi la tua informativa sulla privacy in italiano.
-3
View File
@@ -1,3 +0,0 @@
# Política de Privacidade
Aqui está a tua Política de Privacidade em português.
+2 -8
View File
@@ -37,11 +37,5 @@
} }
} }
}, },
"slogan": "Gebaut zum Suchen.", "slogan": "Gebaut zum Suchen."
}
"legal": {
"privacy": {
"title": "Datenschutzerklärung"
}
}
}
+2 -8
View File
@@ -37,11 +37,5 @@
} }
} }
}, },
"slogan": "Built to search.", "slogan": "Built to search."
}
"legal": {
"privacy": {
"title": "Privacy Policy"
}
}
}
+2 -8
View File
@@ -37,11 +37,5 @@
} }
} }
}, },
"slogan": "Hecho para buscar.", "slogan": "Hecho para buscar."
}
"legal": {
"privacy": {
"title": "Política de Privacidad"
}
}
}
+2 -8
View File
@@ -37,11 +37,5 @@
} }
} }
}, },
"slogan": "Conçu pour chercher.", "slogan": "Conçu pour chercher."
}
"legal": {
"privacy": {
"title": "Politique de Confidentialité"
}
}
}
+2 -8
View File
@@ -37,11 +37,5 @@
} }
} }
}, },
"slogan": "Costruito per cercare.", "slogan": "Costruito per cercare."
}
"legal": {
"privacy": {
"title": "Politica di Privacy"
}
}
}
+2 -8
View File
@@ -37,11 +37,5 @@
} }
} }
}, },
"slogan": "Feito para pesquisar.", "slogan": "Feito para pesquisar."
}
"legal": {
"privacy": {
"title": "Política de Privacidade"
}
}
}
+1 -11
View File
@@ -21,7 +21,6 @@ import SearchView from '../views/SearchView.vue';
import SearchResultsView from '@/features/search/views/SearchResultsView.vue'; import SearchResultsView from '@/features/search/views/SearchResultsView.vue';
import SettingsView from '@/features/settings/views/SettingsView.vue'; import SettingsView from '@/features/settings/views/SettingsView.vue';
import NotFound from '../views/NotFound.vue'; import NotFound from '../views/NotFound.vue';
import PrivacyPolicyView from '@/features/legal/views/PrivacyPolicyView.vue';
const routes = [ const routes = [
{ {
@@ -53,15 +52,6 @@ const routes = [
name: 'notFound', name: 'notFound',
component: NotFound component: NotFound
}, },
{
path: '/privacy',
name: 'privacyPolicy',
component: PrivacyPolicyView,
meta: {
title: () => 'Privacy Policy'
}
},
]; ];
const router = createRouter({ const router = createRouter({
@@ -83,4 +73,4 @@ router.afterEach(to => {
}; };
}); });
export default router; export default router;
+6
View File
@@ -82,6 +82,8 @@ limitations under the License.
--blue-box-shadow: oklch(0.52 0.15 268 / 0.25); --blue-box-shadow: oklch(0.52 0.15 268 / 0.25);
--light-hover: var(--light-d-2); --light-hover: var(--light-d-2);
--invert: invert(0);
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@@ -114,6 +116,8 @@ limitations under the License.
--gray-box-shadow: oklch(0.25 0.0001 271 / 0.7); --gray-box-shadow: oklch(0.25 0.0001 271 / 0.7);
--light-hover: var(--light-d-5); --light-hover: var(--light-d-5);
--invert: invert(1);
} }
} }
@@ -146,4 +150,6 @@ limitations under the License.
--gray-box-shadow: oklch(0.25 0.0001 271 / 0.7); --gray-box-shadow: oklch(0.25 0.0001 271 / 0.7);
--light-hover: var(--light-d-5); --light-hover: var(--light-d-5);
--invert: invert(1);
} }
+122
View File
@@ -0,0 +1,122 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { expect, test } from 'vitest';
import { ensureUnit } from '../cssDimensions';
test.for([
{ dimension: null, expected: '0px' },
{ dimension: undefined, expected: '0px' },
{ dimension: 0, expected: '0px' },
{ dimension: -0, expected: '0px' },
{ dimension: -1, expected: '-1px' },
{ dimension: -42, expected: '-42px' },
{ dimension: 1, expected: '1px' },
{ dimension: 42, expected: '42px' },
{ dimension: 0.0, expected: '0px' },
{ dimension: -0.0, expected: '0px' },
{ dimension: -1.0, expected: '-1px' },
{ dimension: -42.0, expected: '-42px' },
{ dimension: 1.0, expected: '1px' },
{ dimension: 42.0, expected: '42px' },
{ dimension: 0.42, expected: '0.42px' },
{ dimension: -0.42, expected: '-0.42px' },
{ dimension: -1.42, expected: '-1.42px' },
{ dimension: -42.42, expected: '-42.42px' },
{ dimension: 1.42, expected: '1.42px' },
{ dimension: 42.42, expected: '42.42px' },
{ dimension: '0', expected: '0px' },
{ dimension: '-0', expected: '-0px' },
{ dimension: '-1', expected: '-1px' },
{ dimension: '-42', expected: '-42px' },
{ dimension: '1', expected: '1px' },
{ dimension: '42', expected: '42px' },
{ dimension: '0.0', expected: '0.0px' },
{ dimension: '-0.0', expected: '-0.0px' },
{ dimension: '-1.0', expected: '-1.0px' },
{ dimension: '-42.0', expected: '-42.0px' },
{ dimension: '1.0', expected: '1.0px' },
{ dimension: '42.0', expected: '42.0px' },
{ dimension: '0.42', expected: '0.42px' },
{ dimension: '-0.42', expected: '-0.42px' },
{ dimension: '-1.42', expected: '-1.42px' },
{ dimension: '-42.42', expected: '-42.42px' },
{ dimension: '1.42', expected: '1.42px' },
{ dimension: '42.42', expected: '42.42px' },
{ dimension: '0px', expected: '0px' },
{ dimension: '-0px', expected: '-0px' },
{ dimension: '-1px', expected: '-1px' },
{ dimension: '-42px', expected: '-42px' },
{ dimension: '1px', expected: '1px' },
{ dimension: '42px', expected: '42px' },
{ dimension: '0.0px', expected: '0.0px' },
{ dimension: '-0.0px', expected: '-0.0px' },
{ dimension: '-1.0px', expected: '-1.0px' },
{ dimension: '-42.0px', expected: '-42.0px' },
{ dimension: '1.0px', expected: '1.0px' },
{ dimension: '42.0px', expected: '42.0px' },
{ dimension: '0.42px', expected: '0.42px' },
{ dimension: '-0.42px', expected: '-0.42px' },
{ dimension: '-1.42px', expected: '-1.42px' },
{ dimension: '-42.42px', expected: '-42.42px' },
{ dimension: '1.42px', expected: '1.42px' },
{ dimension: '42.42px', expected: '42.42px' },
{ dimension: '0em', expected: '0em' },
{ dimension: '-0em', expected: '-0em' },
{ dimension: '-1em', expected: '-1em' },
{ dimension: '-42em', expected: '-42em' },
{ dimension: '1em', expected: '1em' },
{ dimension: '42em', expected: '42em' },
{ dimension: '0.0em', expected: '0.0em' },
{ dimension: '-0.0em', expected: '-0.0em' },
{ dimension: '-1.0em', expected: '-1.0em' },
{ dimension: '-42.0em', expected: '-42.0em' },
{ dimension: '1.0em', expected: '1.0em' },
{ dimension: '42.0em', expected: '42.0em' },
{ dimension: '0.42em', expected: '0.42em' },
{ dimension: '-0.42em', expected: '-0.42em' },
{ dimension: '-1.42em', expected: '-1.42em' },
{ dimension: '-42.42em', expected: '-42.42em' },
{ dimension: '1.42em', expected: '1.42em' },
{ dimension: '42.42em', expected: '42.42em' },
{ dimension: 'calc(42px - 1em)', expected: 'calc(42px - 1em)' },
{ dimension: 'calc(42px)', expected: 'calc(42px)' },
{ dimension: 'calc(42vh)', expected: 'calc(42vh)' },
{ dimension: 'min(42px, 1em)', expected: 'min(42px, 1em)' },
{ dimension: 'max(42px, 5rem)', expected: 'max(42px, 5rem)' },
{ dimension: 'clamp(42vh, 23vw, 13cap)', expected: 'clamp(42vh, 23vw, 13cap)' },
{ dimension: 'calc(42px-1em)', expected: 'calc(42px-1em)' },
{ dimension: 'min(42px,1em)', expected: 'min(42px,1em)' },
{ dimension: 'max(42px,5rem)', expected: 'max(42px,5rem)' },
{ dimension: 'clamp(42vh,23vw,13cap)', expected: 'clamp(42vh,23vw,13cap)' },
{ dimension: 'clamp( 42vh,23vw,13cap )', expected: 'clamp( 42vh,23vw,13cap )' }
])('ensureUnit returns $expected with input $dimension', ({ dimension, expected }) => {
expect(ensureUnit(dimension)).toBe(expected);
});
@@ -0,0 +1,61 @@
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { expect, test } from 'vitest';
import getCurrentLanguage from '../currentLanguage';
const locales = [
{ navigatorLanguage: 'en', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-US', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-AU', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-FR', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-us', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-au', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'zh-Hant-TW', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'uz-Latn-UZ', localStorageLanguage: null, expected: 'uz'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE-u-co-phonebk', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'en', localStorageLanguage: 'de', expected: 'de'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: 'fr', expected: 'fr'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: 'zh', expected: 'zh'}
];
test.for(locales)('returns the language $expected (navigator: $navigatorLanguage; local storage: $localStorageLanguage)', ({ navigatorLanguage, localStorageLanguage, expected }) => {
Object.defineProperty(navigator, 'language', {
value: navigatorLanguage,
configurable: true
});
if (localStorageLanguage) {
localStorage.setItem('locale', localStorageLanguage);
};
expect(getCurrentLanguage()).toBe(expected);
});
@@ -12,4 +12,20 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export const ensureUnit = function (d) {
if (!d) d = 0;
// ensure that it is a string
if (typeof d === 'number') d = d.toString();
if (d === '') d = '0';
// if it ends with a number
if (/\d$/.test(d)) {
d += 'px';
};
return d;
};
+3 -2
View File
@@ -67,9 +67,10 @@ const submitSearch = function () {
max-width: 100%; max-width: 100%;
} }
.slogan{ .slogan {
margin: 0; margin-top: 1rem;
font-size: small; font-size: small;
line-height: normal;
} }
.search-container { .search-container {
+7
View File
@@ -21,6 +21,9 @@ import { createHtmlPlugin } from 'vite-plugin-html';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
build: {
target: 'es2020'
},
plugins: [ plugins: [
vue(), vue(),
createHtmlPlugin({ createHtmlPlugin({
@@ -31,5 +34,9 @@ export default defineConfig({
alias: { alias: {
'@': path.resolve(__dirname, './src') '@': path.resolve(__dirname, './src')
} }
},
test: {
globals: false,
environment: 'jsdom'
} }
}) })