470 Commits

Author SHA1 Message Date
jakob.scheid 980805730a feat(settings): go to the settings route if the settings sub-route does not exist 2026-06-05 15:31:25 +02:00
jakob.scheid 96d4d92f68 feat(settings): focus sidebar section button that is currently active 2026-06-05 15:28:57 +02:00
jakob.scheid f947c4bd33 feat(settings): remove sidebar section link button container 2026-06-05 15:18:48 +02:00
jakob.scheid 2c81a7cdf7 feat(settings): show first-level sections in the sidebar 2026-06-05 13:51:58 +02:00
jakob.scheid 1a13b78eff add common class for button links 2026-06-05 13:49:56 +02:00
jakob.scheid c38b38707c underline focused links that are no buttons 2026-06-05 13:47:14 +02:00
jakob.scheid 3ea55d24dc add button CSS class 2026-06-05 13:45:54 +02:00
jakob.scheid 1f4037f64c feature(router): remove trailing slash(es) 2026-06-05 13:45:31 +02:00
jakob.scheid cf781ff3f5 feature(settings): load settings configuration 2026-06-05 13:45:11 +02:00
jakob.scheid 8acb675a03 fix(footer): update settings router link target 2026-06-05 13:02:26 +02:00
jakob.scheid 8d6e87af16 feature(settings): catch all subpaths of the settings path in the router 2026-06-05 12:51:08 +02:00
jakob.scheid eea417013d Merge pull request 'Allow only sections as first-level settings' (#117) from feature/restrict-first-level-settings into main
Deploy on dev / Deploy on dev (push) Successful in 39s
Reviewed-on: #117
Reviewed-by: Jakob Gregory
2026-06-05 12:29:51 +02:00
jakob.scheid 192f3c37a9 feat(settings): add test cases to test that only sections are allowed as first-level settings 2026-06-05 11:37:17 +02:00
jakob.scheid b729b85ad0 feat(settings): allow only sections as first-level settings 2026-06-05 11:35:41 +02:00
jakob.scheid a9f805e1fd Merge pull request 'Fix font scaling on Not Found page by removing vw units' (#115) from feature/fix-not-found-font-scaling into main
Deploy on dev / Deploy on dev (push) Successful in 39s
Reviewed-on: #115
Reviewed-by: Jakob Scheid
2026-06-04 18:54:44 +02:00
johannes.vos af418197a4 style(not-found): replace 2vw font size with stable 1.75rem dimension 2026-06-04 18:41:15 +02:00
jakob.scheid 8a718b4bab Merge pull request 'Add settings validator unit test' (#113) from testing/settings-validator into main
Deploy on dev / Deploy on dev (push) Successful in 42s
Reviewed-on: #113
Reviewed-by: Jakob Gregory
2026-06-04 17:48:08 +02:00
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
jakob.scheid 3b8c387c44 Merge pull request 'Move the search bar in the search results view into the navigation bar' (#82) from chore/searchbar into main
Deploy on dev / Deploy on dev (push) Successful in 33s
Reviewed-on: #82
Reviewed-by: Jakob Scheid
2026-05-29 18:30:05 +02:00
jakob.scheid d461da90f2 Make search query model cleaner 2026-05-29 18:28:02 +02:00
jakob.scheid 295bfc19e6 Increase navbar vertical padding 2026-05-29 18:16:28 +02:00
jakob.scheid 7a7f698b44 Set navbar height to prevent layout shifting when the search bar is shown when the search results view is visible 2026-05-29 18:13:39 +02:00
jakob.scheid 2fd010ddfa Remove redundant search bar component 2026-05-29 18:08:21 +02:00
jakob.scheid f72a2bf2b1 Remove unnecessary whitespaces 2026-05-29 16:18:40 +02:00
johannes.vos 926010f128 chore(search-results): clean up unused search props and imports 2026-05-29 15:56:47 +02:00
johannes.vos 98c954e361 feat(navbar): show search input only on search results page 2026-05-29 15:55:38 +02:00
johannes.vos 325551d253 copy/paste searchbar.vue code to searchbar-searchresults.vue (placeholder) 2026-05-29 15:42:09 +02:00
johannes.vos a33bc047fa move Searchbar-SearchResults.vue file to nav/components 2026-05-29 15:41:33 +02:00
johannes.vos 1421789e43 create empty Searchbar-SearchResults.vue file 2026-05-29 15:37:40 +02:00
jakob.scheid 27a22ce569 Merge pull request 'add settings configuration parser' (#78) from feature/settings-config-parser into main
Deploy on dev / Deploy on dev (push) Successful in 37s
Reviewed-on: #78
Reviewed-by: Jakob Schei <1+jakob.scheid@noreply.localhost>
2026-05-29 14:03:15 +02:00
jakob.scheid b28af20d11 Merge branch 'main' into feature/settings-config-parser 2026-05-29 14:02:59 +02:00
jakob.scheid c81de2dce3 Merge pull request 'Bug/Link-color' (#81) from bug/link-color into main
Deploy on dev / Deploy on dev (push) Successful in 32s
Reviewed-on: #81
Reviewed-by: Jakob Scheid
2026-05-29 13:22:44 +02:00
johannes.vos 1b60984b5c add style - make links white/black 2026-05-28 21:48:22 +02:00
johannes.vos 733bc2b16a add class to link 2026-05-28 21:47:46 +02:00
jakob.scheid 11800f6ef4 feat(settings): Remove old JSDoc typedefs 2026-05-26 19:57:36 +02:00
jakob.scheid b47ab0355e feat(settings): Remove parameter 'url' from the loading function of the settings composable 2026-05-26 19:55:45 +02:00
jakob.scheid 34a7cb3f2c feat(settings): Make setting default value optional 2026-05-26 19:51:45 +02:00
johannes.vos da76d14d15 WIP 2026-05-26 14:55:18 +02:00
johannes.vos 547bc241da feat(settings): use type field as discriminant in JSDoc type definitions 2026-05-26 14:55:17 +02:00
johannes.vos 4ef6008976 feat(settings): default allowMultiple to false if not specified 2026-05-26 14:55:11 +02:00
johannes.vos 035aa1aa77 feat(settings): replace fetch with dynamic import for settings.json 2026-05-26 14:53:52 +02:00
johannes.vos 462ae00506 feat(settings): move settings.json to src and remove example content 2026-05-26 14:53:52 +02:00
jakob.scheid 5d2064134b feat(settings): Make setting default value optional 2026-05-26 12:21:54 +02:00
johannes.vos aeb348fcfd feat(settings): add example settings.json to public/ 2026-05-26 11:37:34 +02:00
johannes.vos 37ca0baa6d add emty public/settings.json 2026-05-26 11:36:26 +02:00
johannes.vos a165c13d7c feat(settings): add useSettingsConfig composable 2026-05-26 11:35:35 +02:00
johannes.vos 1378813783 add emtpy useSettingsConfig.js 2026-05-26 11:35:26 +02:00
johannes.vos e376e9b362 feat(settings): add settings config loader/parser 2026-05-26 11:34:56 +02:00
johannes.vos d09514f71f add emtpy settingsParser.js 2026-05-26 11:34:40 +02:00
johannes.vos f6d72dbea3 feat(settings): add settings config validator 2026-05-26 11:34:14 +02:00
johannes.vos 0d7814d655 add emty settingsValidator.js 2026-05-26 11:33:58 +02:00
johannes.vos c387c1486f feat(settings): add JSDoc type definitions for settings config 2026-05-26 11:32:51 +02:00
johannes.vos 878b558603 add emty settingsConfig.js 2026-05-26 11:31:41 +02:00
jakob.scheid 0251d5c577 Merge pull request 'Restructure assets directory' (#76) from refactor/restructure-assets-directory into main
Deploy on dev / Deploy on dev (push) Successful in 32s
Reviewed-on: #76
Reviewed-by: Jakob Gregory
2026-05-23 13:29:04 +02:00
jakob.scheid 280dd9d7c0 Remove .gitkeep in the assets directory 2026-05-23 13:21:38 +02:00
jakob.scheid 8e2fc6bfa5 Move image assets into a subdirectory for images in the assets directory 2026-05-23 13:21:21 +02:00
jakob.scheid 69d752f79b Merge pull request 'Add base settings page' (#74) from feature/base-settings-page into main
Deploy on dev / Deploy on dev (push) Successful in 30s
Reviewed-on: #74
Reviewed-by: Jakob Gregory
2026-05-23 13:16:37 +02:00
jakob.scheid 4d62ca2b75 Merge branch 'main' into feature/base-settings-page 2026-05-23 13:16:26 +02:00
jakob.scheid f5b88df831 Merge pull request 'Remove license placeholders' (#75) from bugfix/remove-placeholders-in-license-files into main
Deploy on dev / Deploy on dev (push) Successful in 35s
Reviewed-on: #75
Reviewed-by: Jakob Gregory
2026-05-23 13:14:02 +02:00
jakob.scheid bd25409d9f Add license header to left sidebar layout 2026-05-23 12:08:05 +02:00
jakob.scheid fe20a618fe Add license header to sidebar 2026-05-23 12:07:45 +02:00
jakob.scheid 17b49cdb6e Add license header to settings view 2026-05-23 12:07:32 +02:00
jakob.scheid 287d7ad713 Add padding to the settings view header 2026-05-23 12:03:03 +02:00
jakob.scheid d3e4d54c57 Add padding to the sidebar 2026-05-23 11:58:53 +02:00
jakob.scheid ead8821b0a Use semantic HTML elements in the left sidebar layout 2026-05-23 11:57:23 +02:00
jakob.scheid d3e8a0125d Use semantic HTML elements in the sidebar 2026-05-23 11:56:55 +02:00
jakob.scheid 34b7cfbf5a Use semantic HTML elements in the settings view 2026-05-23 11:53:47 +02:00
jakob.scheid 1ebf5780d2 Move main content padding frm the app root component to the views 2026-05-23 11:51:49 +02:00
jakob.scheid f354c1867c Add left sidebar layout top border 2026-05-23 11:46:28 +02:00
jakob.scheid 0583de0dfa Add settings view page wrapper 2026-05-23 11:43:53 +02:00
jakob.scheid 2a5e79b55d Add padding to the main content in the left sidebar layout 2026-05-23 11:42:45 +02:00
jakob.scheid 4df51a970d Include sidebar in the left sidebar layout 2026-05-23 11:42:25 +02:00
jakob.scheid 7013c60a20 Remove top margin from footer 2026-05-23 11:40:58 +02:00
jakob.scheid 3623265ae0 Remove main content bottom padding 2026-05-23 11:40:32 +02:00
jakob.scheid fbefb2e7fd Rename base layout to left sidebar layout 2026-05-23 11:40:03 +02:00
jakob.scheid b0d3258369 Remove main content wrapper 2026-05-23 11:26:06 +02:00
jakob.scheid f65c3d58aa Add containers to components without a content container 2026-05-23 11:25:48 +02:00
jakob.scheid 33e9ca7cf1 Add check whether there is a sidebar component in the base layout 2026-05-23 10:45:28 +02:00
jakob.scheid def3a84e59 Use base layout in the app root component 2026-05-23 10:44:58 +02:00
jakob.scheid b48a487d2f Rename left sidebar layout to base layout 2026-05-23 03:54:53 +02:00
jakob.scheid 76815c6a93 Add sidebar to the settings view 2026-05-23 03:32:32 +02:00
jakob.scheid aa47015d3a Add sidebar right border 2026-05-23 03:30:18 +02:00
jakob.scheid 989664422c Add sidebar container 2026-05-23 03:28:59 +02:00
jakob.scheid 2ad95385d2 Add CSS grid container in the left sidebar layout 2026-05-23 03:27:44 +02:00
jakob.scheid b00aaabbb1 Add sidebar component slot 2026-05-23 03:13:52 +02:00
jakob.scheid 5104c4e8e7 Add left sidebar layout content 2026-05-23 03:13:34 +02:00
jakob.scheid d1c428eccd Rename file 2026-05-23 03:10:37 +02:00
jakob.scheid ed2b3224b4 Remove sidebar section component 2026-05-23 03:09:15 +02:00
jakob.scheid 6cc94b651f Bugfix: Fix spelling mistake in the sidebar section component 2026-05-23 03:07:40 +02:00
jakob.scheid 16ee164d76 Add sidebar section boilerplate 2026-05-23 02:41:04 +02:00
jakob.scheid 2989817278 Use sidebar component in left sidebar layout 2026-05-23 02:38:56 +02:00
jakob.scheid 0a65bcdae1 Add left sidebar layout boilerplate 2026-05-23 02:36:33 +02:00
jakob.scheid d5601b8fdb Add sidebar boilerplate 2026-05-23 02:35:16 +02:00
jakob.scheid be96541b3e Replace hard-coded settings link text in the footer with a translation 2026-05-23 02:29:06 +02:00
jakob.scheid ad8bbfa666 Update footer flexbox 2026-05-23 02:27:43 +02:00
jakob.scheid b324b93141 Reduce footer height 2026-05-23 02:25:58 +02:00
jakob.scheid 7bcdf77ca8 Add link to settings page in the footer 2026-05-23 02:25:36 +02:00
jakob.scheid 6726600c8e Add settings view heading 2026-05-23 02:22:40 +02:00
jakob.scheid 027ee2d191 Add settings route 2026-05-23 02:21:37 +02:00
jakob.scheid 6d5c7e4270 Add translations for 'settings' 2026-05-23 02:17:53 +02:00
jakob.scheid 8901967ab0 Add settings view boilerplate 2026-05-23 02:13:41 +02:00
jakob.scheid e177ed279c Remove license placeholder in the search results view 2026-05-23 02:10:02 +02:00
jakob.scheid 923942f1b1 Remove license placeholder in the footer 2026-05-23 02:09:39 +02:00
jakob.scheid 539991fac7 Remove license placeholder in the not found view 2026-05-23 02:09:18 +02:00
jakob.scheid 9e049bc5c9 Remove license placeholder in the logo graphic 2026-05-23 02:09:07 +02:00
jakob.scheid 495c2379ce Merge pull request 'Change Colors' (#65) from bug/colors into main
Deploy on dev / Deploy on dev (push) Successful in 31s
Reviewed-on: #65
Reviewed-by: Jakob Scheid
2026-05-23 02:05:53 +02:00
jakob.scheid c50fe25d04 Add CSS variables for light hover background 2026-05-22 21:13:05 +02:00
jakob.scheid 1300ab46a0 Use the --border CSS variable in the language switch button component 2026-05-22 17:51:56 +02:00
jakob.scheid 28fcfa1f92 Use --border variable in the footer 2026-05-22 17:50:13 +02:00
jakob.scheid f808525d56 Merge branch 'main' into bug/colors 2026-05-22 17:46:56 +02:00
jakob.scheid 1c7a0c0e58 Use the CSS variable for dark in the navbar right links 2026-05-22 17:46:35 +02:00
jakob.scheid 0eccd33919 Add CSS variable for blue box shadow 2026-05-22 17:45:46 +02:00
jakob.scheid 1f67e95735 Add CSS variable for gray box shadow 2026-05-22 17:44:06 +02:00
jakob.scheid 3c82baca67 Use variable for the search results view error message background 2026-05-22 17:35:22 +02:00
jakob.scheid 6d95601399 Add CSS variable for borders 2026-05-22 17:33:03 +02:00
jakob.scheid 9aecc195a9 Merge pull request 'Add more Languages and switch button' (#70) from feature/add-languages into main
Deploy on dev / Deploy on dev (push) Successful in 29s
Reviewed-on: #70
Reviewed-by: Jakob Scheid
2026-05-22 17:19:25 +02:00
jakob.scheid 2bf433621a Remove flags from language labels 2026-05-22 17:18:15 +02:00
jakob.scheid abbfd0ad9d Include flags in the language name translations instead of hard-coding it in the language switch button component 2026-05-22 15:20:08 +02:00
jakob.scheid 619d4065b3 Align right navbar links vertically centered 2026-05-22 14:20:49 +02:00
jakob.scheid 3ca938d65b Update border color of language button 2026-05-22 14:19:12 +02:00
jakob.scheid 666ecb4e98 Update border color of language dropdown 2026-05-22 14:17:25 +02:00
jakob.scheid 3af6cb3c16 Remove CSS font-size property 2026-05-21 23:33:22 +02:00
jakob.scheid 006bb5136e Add feature to close the language switching dropdown when the user clicks outside of it 2026-05-21 23:28:42 +02:00
jakob.scheid 258a4025bc Use translations in the translation files instead of hard-coded strings for the language names 2026-05-21 22:32:39 +02:00
jakob.scheid 420f47dca4 Format SUPPORTED_LANGUAGES more readable 2026-05-21 22:06:23 +02:00
jakob.scheid 6f164a0256 Remove unsupported languages from SUPPORTED_LANGUAGES 2026-05-21 22:06:02 +02:00
jakob.scheid b30aea57ad Add list with languages written from right to left 2026-05-21 22:04:58 +02:00
jakob.scheid be19a3c29e Speak English in a comment 2026-05-21 21:59:59 +02:00
jakob.scheid f08acad086 Address the user formally in the German translations 2026-05-21 21:57:05 +02:00
jakob.scheid 90f99e4240 Rename 'src/features/language/' to 'src/features/i18n/' 2026-05-21 15:45:11 +02:00
johannes.vos 46d92675eb save lanugage in local storage 2026-05-20 20:33:07 +02:00
johannes.vos dc532d3848 implement switch button 2026-05-20 20:31:45 +02:00
johannes.vos cb42c9d368 remove lanugages 2026-05-20 20:31:24 +02:00
johannes.vos 6fa3ba6298 add switch button 2026-05-20 20:26:51 +02:00
johannes.vos 7c8cf6406d add content in langauge files 2026-05-20 20:25:26 +02:00
johannes.vos 010d29c74e add emty LangaugeSwitchButton.bue file 2026-05-20 20:22:10 +02:00
johannes.vos 5a04e2a2f1 Add suported languages in i18n.js 2026-05-20 20:20:57 +02:00
johannes.vos a7ff5e2bf4 Add emty langauge json files 2026-05-20 20:17:25 +02:00
jakob.scheid 9e4c9febdb Merge pull request 'Use vue-i18n' (#68) from feature/vue-i18n into main
Deploy on dev / Deploy on dev (push) Successful in 32s
Reviewed-on: #68
Reviewed-by: Johannes D. Vos
2026-05-20 17:35:02 +02:00
jakob.scheid 2b136e30c8 Add translation for the slogan 2026-05-19 21:56:19 +02:00
jakob.scheid 16e69ff072 Add translations for the color scheme button 2026-05-19 21:53:33 +02:00
jakob.scheid c8f8022f44 Add translation for the back to search link on the not found view 2026-05-19 21:47:49 +02:00
jakob.scheid ffaa6200c3 Add translation for the page not found error 2026-05-19 21:46:58 +02:00
jakob.scheid a60526e6d7 Add translations for search errors 2026-05-19 21:43:54 +02:00
jakob.scheid 5d7181bea2 Add translations for error messages 2026-05-19 21:27:58 +02:00
jakob.scheid dd0f560bc8 Add translations for the search bar 2026-05-19 21:14:07 +02:00
jakob.scheid ced76720cc Add English translations JSON file 2026-05-19 21:07:44 +02:00
jakob.scheid 94fc328737 Add error handling if a locale to be loaded does not exist 2026-05-19 21:06:45 +02:00
jakob.scheid 2eb187ec1a Add function to load languages dynamically 2026-05-19 21:05:09 +02:00
jakob.scheid 2882f78990 Adapt the language to the browser language 2026-05-19 19:47:58 +02:00
jakob.scheid 78cde4641c Fix: Replace placeholer in the license header of 'src/i18n.js' with the actual values 2026-05-19 19:43:14 +02:00
jakob.scheid f37d403636 Add base i18n 2026-05-19 19:39:56 +02:00
jakob.scheid 09c645e657 Add dependency 'vue-i18n' 2026-05-19 19:25:59 +02:00
johannes.vos 66483a3a6a Add Border 2026-05-19 19:11:27 +02:00
johannes.vos d5c714cbd8 Change Colors of Footer 2026-05-19 19:09:51 +02:00
johannes.vos d8a4c0023b Change Colors of Footer 2026-05-19 19:07:24 +02:00
johannes.vos 5b8c13c8cf Remove Background 2026-05-19 18:59:39 +02:00
johannes.vos b172e3cf2b Change Backgorund Color 2026-05-19 18:57:18 +02:00
johannes.vos 46a2caf845 Make Border of Button visible / lighter in DarkMode 2026-05-19 18:55:01 +02:00
johannes.vos 63d14ddefd Change Black Varbiable to gray 2026-05-19 18:49:00 +02:00
jakob.scheid ba45c0d488 Merge pull request 'Add option to adapt color scheme to the system color scheme' (#61) from feature/dynamic-dark-mode into main
Deploy on dev / Deploy on dev (push) Successful in 37s
Reviewed-on: #61
Reviewed-by: Jakob Gregory
2026-05-18 16:55:40 +02:00
jakob.scheid a7d1cc0f62 Set text color of inputs according to the color scheme 2026-05-17 21:05:31 +02:00
jakob.scheid 28fe027517 Set CSS property 'color-scheme' according to the current color scheme 2026-05-17 21:01:48 +02:00
jakob.scheid 05b6a5d513 Remove duplicated CSS rule '#app' 2026-05-17 20:59:37 +02:00
jakob.scheid c78357b61d Use the term 'color scheme' instead of 'color theme' 2026-05-17 20:52:55 +02:00
jakob.scheid 678c41e990 Set dark mode dynamically 2026-05-17 20:43:13 +02:00
jakob.scheid 6fb3d95cd5 Set color scheme button text color to dark 2026-05-17 20:41:44 +02:00
jakob.scheid 7f5cbf5665 Update color variables stylesheet 2026-05-17 20:40:53 +02:00
jakob.scheid 555bdb0cfb Remove appHasDarkClass function 2026-05-17 20:38:19 +02:00
jakob.scheid cd67bf486f Add color theme button 'auto' state 2026-05-17 17:54:09 +02:00
jakob.scheid c1525cd2f2 Add auto fallbacks 2026-05-17 17:34:35 +02:00
jakob.scheid 5d98f27b6b Make color theme switch more flexible 2026-05-17 17:33:22 +02:00
jakob.scheid 170f188435 Move color theme toggle button into a separate component 2026-05-17 15:24:53 +02:00
jakob.scheid c7fd0fe132 Merge pull request 'Add license headers' (#58) from chore/license-headers into main
Deploy on dev / Deploy on dev (push) Successful in 28s
Reviewed-on: #58
Reviewed-by: Johannes D. Vos
2026-05-17 14:37:40 +02:00
jakob.scheid 3313ed40e6 Add license headers 2026-05-17 14:30:50 +02:00
jakob.scheid 50c0ff4a8c Merge pull request 'Add dev deployment workflow #49' (#57) from chore/gitea-actions-workflow into main
Deploy on dev / Deploy on dev (push) Successful in 27s
Reviewed-on: #57
Reviewed-by: Johannes D. Vos
2026-05-17 14:23:08 +02:00
jakob.scheid 5e6ff65e68 Merge branch 'main' into chore/gitea-actions-workflow 2026-05-17 13:29:40 +02:00
jakob.scheid 8d2937d4a0 Add dev deployment workflow upload 2026-05-17 13:19:04 +02:00
jakob.scheid 7273a732b1 Add dev deployment workflow build step 2026-05-17 13:18:49 +02:00
jakob.scheid 62c6784e2c Add dev deployment workflow dependencies installation step 2026-05-17 13:18:37 +02:00
jakob.scheid c4d32c2815 Add dev deployment workflow boilerplate 2026-05-17 13:18:21 +02:00
jakob.scheid ceda686d03 Merge pull request 'use logo' (#52) from feature/use-logo into main
Reviewed-on: #52
Reviewed-by: Jakob Scheid
2026-05-15 18:53:03 +02:00
jakob.scheid 792b4b80b8 Remove comment 2026-05-15 18:50:28 +02:00
johannes.vos 22e50adaa5 fix: import logo in SearchView and use :src binding 2026-05-15 18:22:47 +02:00
johannes.vos 8eab67b78f fix: import logo in SearchView and use :src binding 2026-05-15 18:22:47 +02:00
jakob.scheid 6e3ef7aae0 Merge branch 'main' into feature/use-logo 2026-05-15 18:08:52 +02:00
jakob.scheid 3ef2b5b089 Merge pull request 'Change icons to toggle color theme to something more simple' (#54) from ui/change-icons-nav into main
Reviewed-on: #54
Reviewed-by: Jakob Scheid
2026-05-15 18:06:57 +02:00
johannes.vos 551327b018 Change Icons of Toggle to more simpler 2026-05-15 17:47:15 +02:00
johannes.vos d7246b9aab change height of logo 2026-05-15 17:25:37 +02:00
johannes.vos 5eddee4d4d feat: replace text with logo image in Navbar 2026-05-15 17:24:50 +02:00
johannes.vos ae639dbe71 feat: replace h1 text with logo image in SearchView 2026-05-15 17:23:59 +02:00
johannes.vos 31dd40e1d7 add svg file 2026-05-15 17:22:54 +02:00
jakob.scheid 4bdfa0a872 Merge pull request 'add DarkMode with Button' (#51) from feature/dark-mode into main
Reviewed-on: #51
Reviewed-by: Jakob Scheid
2026-05-15 12:47:25 +02:00
johannes.vos 488186d795 fix: restore full-height flex layout after dark mode wrapper 2026-05-15 10:21:30 +02:00
johannes.vos 67adb17936 feat: persist dark mode preference in localStorage 2026-05-15 10:19:44 +02:00
johannes.vos 2af6c74696 style: add dark mode toggle button styles 2026-05-15 10:14:39 +02:00
johannes.vos 81cebb4636 feat: add dark mode toggle button to Navbar 2026-05-15 10:14:05 +02:00
johannes.vos d24e11b466 feat: inject isDark in Navbar 2026-05-15 10:13:36 +02:00
johannes.vos a0eaf36db3 add isDark state in App.vue 2026-05-15 10:12:23 +02:00
johannes.vos 0b609598f0 style: add dark mode CSS varivale 2026-05-15 10:09:56 +02:00
jakob.scheid fc23a6d185 Merge pull request 'Add NotFound.vue #48' (#50) from feature/not-found-page into main
Reviewed-on: #50
Reviewed-by: Jakob Scheid
2026-05-14 16:21:30 +02:00
johannes.vos cf7525b32a Add style for the content 2026-05-14 16:06:23 +02:00
johannes.vos 6175e3b0be Remove unnecessary Style 2026-05-14 16:05:13 +02:00
johannes.vos 22c3aafcc5 Remove header 2026-05-14 16:04:38 +02:00
johannes.vos 7b601416ad Make the error code more human-readable 2026-05-14 16:02:22 +02:00
johannes.vos 7098414db1 Remove <div class=search-content> 2026-05-14 15:59:27 +02:00
johannes.vos a45e208f9b Remove space 2026-05-14 15:58:08 +02:00
johannes.vos f152d39db8 fix: use camelCase for not-found route name 2026-05-14 15:55:58 +02:00
johannes.vos 08c3de5cb2 Add Link Style 2026-05-14 12:36:57 +02:00
johannes.vos 61e4f3d1f2 Rename class 'slogan' to 'error-message' 2026-05-14 12:27:15 +02:00
johannes.vos 6aca8a6087 Add RouterLink to Searchview.vue 2026-05-14 12:25:45 +02:00
johannes.vos fb5985dcb1 Remove unnecesary script 2026-05-14 12:24:02 +02:00
johannes.vos 78679fe03b Add script 2026-05-14 12:22:58 +02:00
johannes.vos 06f7037e11 chore: update package-lock.json 2026-05-14 12:22:08 +02:00
johannes.vos 9b04b00c11 merge: resolve conflict, add NotFound route 2026-05-14 12:21:48 +02:00
johannes.vos 2956c54496 Make .slogan bigger 2026-05-14 12:19:47 +02:00
johannes.vos ba6e8e652d Copy style from SearchView.vue 2026-05-14 12:17:12 +02:00
johannes.vos 237fcd33dc Remove unnecessary Form 2026-05-14 12:15:50 +02:00
johannes.vos d2302afdc4 Change Text in NotFound.vue 2026-05-14 12:15:10 +02:00
johannes.vos 6f93d42b02 Fix Router bug 2026-05-14 12:14:12 +02:00
johannes.vos 25958bf7bb Add Router 2026-05-14 12:12:36 +02:00
johannes.vos 8d93ba2638 Add template in NotFound.vue 2026-05-14 12:10:39 +02:00
johannes.vos 336a063d1e Add NotFound.vue 2026-05-14 12:08:55 +02:00
jakob.scheid cda4d58486 Merge pull request 'Add search form submit #32' (#45) from feature/search-form-submit into main
Reviewed-on: #45
Reviewed-by: Jakob Gregory
2026-05-14 11:29:18 +02:00
jakob.scheid e7986146a1 Merge branch 'main' into feature/search-form-submit 2026-05-14 11:28:42 +02:00
jakob.scheid d58845dad9 Merge pull request 'Use HTML minifier from vite-plugin-html #46' (#47) from chore/use-html-minifier into main
Reviewed-on: #47
Reviewed-by: Johannes D. Vos
2026-05-13 16:42:11 +02:00
jakob.scheid cd1ed0ab3f Include licenses for vite-plugin-html and its dependencies 2026-05-11 19:20:41 +02:00
jakob.scheid 40b68dd52d Use HTML minifier from vite-plugin-html 2026-05-11 19:18:19 +02:00
jakob.scheid c1f1099848 Use CSS variables 2026-05-11 18:41:25 +02:00
jakob.scheid 926c1fc463 Merge branch 'main' into feature/search-form-submit 2026-05-11 15:20:05 +02:00
jakob.scheid 3e605f60d0 Merge pull request 'Use CSS variables #33' (#44) from refactor/css-variables into main
Reviewed-on: #44
Reviewed-by: Jakob Gregory
2026-05-11 14:18:34 +02:00
jakob.scheid 49d4401d00 Add search results error message outline 2026-05-11 00:12:27 +02:00
jakob.scheid 587a97fa03 Add search results error message animation 2026-05-11 00:11:04 +02:00
jakob.scheid 4d5ddfd7ea Add search results error message 2026-05-11 00:10:49 +02:00
jakob.scheid c5af7366a9 Update search bar width at the start page 2026-05-11 00:06:45 +02:00
jakob.scheid cab550051f Add search bar with search query to the search results view 2026-05-10 19:47:23 +02:00
jakob.scheid d407c2f0e9 Add padding to the main content 2026-05-10 19:46:46 +02:00
jakob.scheid 261657d343 Move search form to the search bar component 2026-05-10 19:45:56 +02:00
jakob.scheid e3117110e9 Move flexbox to the start page view 2026-05-10 19:04:49 +02:00
jakob.scheid 1097694a8e Add router link to the startpage in the navbar 2026-05-10 17:27:57 +02:00
jakob.scheid 9f60c23c60 Add search submit callback 2026-05-10 17:22:11 +02:00
jakob.scheid 399d7caabc Add search query model 2026-05-10 17:11:35 +02:00
jakob.scheid 679adb4cf9 Use class instead of ID for the search form 2026-05-10 16:52:25 +02:00
jakob.scheid 8f48f3dcb5 Add page title router hook 2026-05-10 16:50:29 +02:00
jakob.scheid 8cdf16f144 Add search results route 2026-05-10 16:40:48 +02:00
jakob.scheid 24926b7312 Add search results view 2026-05-10 16:09:51 +02:00
jakob.scheid 2ed25157e3 Rename startpage route to startPage 2026-05-10 16:07:21 +02:00
jakob.scheid f7f8cda2a5 Add @ alias 2026-05-10 16:06:45 +02:00
jakob.scheid 95c383d58a Apply CSS variables to the search bar 2026-05-10 15:35:54 +02:00
jakob.scheid 933f5ace80 Bug fix: remove commas from the CSS variables starting with --black 2026-05-10 15:32:31 +02:00
jakob.scheid 7ea64524d1 Update light colors 2026-05-10 15:26:39 +02:00
jakob.scheid 6450c12600 Update dark colors 2026-05-10 15:24:44 +02:00
jakob.scheid 94cdf6aa99 Use CSS variables in the footer 2026-05-10 15:23:07 +02:00
jakob.scheid 2718227745 Update black shades 2026-05-10 15:21:37 +02:00
jakob.scheid 257b7ed886 Update white shades 2026-05-10 15:20:06 +02:00
jakob.scheid 0e1148094a Add background colors 2026-05-10 15:12:05 +02:00
jakob.scheid 61c3d2ff7c Add dark colors 2026-05-10 13:41:57 +02:00
jakob.scheid de20ae89fc Add light colors 2026-05-10 13:28:54 +02:00
jakob.scheid 13f026eaab Add dark colors 2026-05-10 13:28:15 +02:00
jakob.scheid 9cdca85a4d Add shades of white 2026-05-10 13:27:16 +02:00
jakob.scheid 69453b947d Add shades of black 2026-05-10 13:27:03 +02:00
jakob.scheid 0e8a49969a Use color variables 2026-05-10 13:22:32 +02:00
jakob.scheid 32bfcf6b33 Add color variables 2026-05-10 13:22:20 +02:00
jakob.scheid 7a09198e9e Merge pull request 'Set maximum width of the search bar #42' (#43) from feature/limit-searchbar-width into main
Reviewed-on: #43
Reviewed-by: Johannes D. Vos
2026-05-10 00:50:16 +02:00
jakob.scheid b1812764f6 Set maximum width for the search form 2026-05-08 20:00:06 +02:00
jakob.scheid e69bbf361b Bug fix: Search bar width did not include the additional padding at the left 2026-05-08 19:53:54 +02:00
jakob.scheid 6a9c5d4035 Merge pull request 'Add footer #27' (#41) from feature/footer into main
Reviewed-on: #41
Reviewed-by: Johannes D. Vos
2026-05-08 18:58:15 +02:00
jakob.scheid a243c0bf9a Merge branch 'main' into feature/footer 2026-05-07 23:32:43 +02:00
jakob.scheid 0da2d0df76 Add copyright note 2026-05-07 23:24:52 +02:00
jakob.scheid 3d9a509581 Merge pull request 'Fix search bar input field height #36' (#38) from bugfix/searchbar-input-field-height into main
Reviewed-on: #38
Reviewed-by: Johannes D. Vos
Reviewed-by: Jakob Gregory
2026-05-07 21:11:27 +02:00
jakob.scheid 6e7e512a9b Move .main-content container from the search view to App.vue 2026-05-07 21:10:55 +02:00
jakob.scheid 41500560f3 Avoid that there is any space between the footer and the viewport bottom 2026-05-07 20:09:47 +02:00
jakob.scheid a1ddf0d145 Make app container a flexbox 2026-05-07 20:08:53 +02:00
jakob.scheid 0f8d974d31 Remove .content-background container 2026-05-07 20:08:19 +02:00
jakob.scheid 703410e0e3 Add footer component boilerplate 2026-05-07 19:37:14 +02:00
jakob.scheid f2a941efca Merge branch 'main' into bugfix/searchbar-input-field-height 2026-05-07 19:13:04 +02:00
jakob.scheid 9799a40958 Merge pull request 'Add license headers in source code files #29' (#30) from feature/add-licenses into main
Reviewed-on: #30
Reviewed-by: Johannes D. Vos
2026-05-07 19:11:46 +02:00
jakob.scheid ecc6598167 Set search bar input height 2026-05-07 18:39:42 +02:00
jakob.scheid 4edc240b10 Rename CSS variable
Rename the variable --submit-button-content-height in the search bar
component to --content-height
2026-05-07 18:36:55 +02:00
jakob.scheid e4ba19bc9b Make searchbar higher 2026-05-07 18:24:15 +02:00
jakob.scheid 93aec1c44d Make searchbar wider 2026-05-07 18:22:51 +02:00
jakob.scheid bb13b22810 Add license headers in sourc code files 2026-05-07 17:56:24 +02:00
jakob.scheid 1a3d1080b9 Merge pull request 'Migrate to Vue.js' (#28) from feature/migrate-to-vue-js into main
Reviewed-on: #28
Reviewed-by: Jakob Gregory
2026-05-07 14:20:13 +02:00
jakob.scheid f846307eee Merge branch 'main' into feature/migrate-to-vue-js 2026-05-07 14:13:46 +02:00
jakob.scheid d54b913bfd Remove old index.html 2026-05-06 22:27:48 +02:00
jakob.scheid 3585b7fc1f Add navbar styles 2026-05-06 22:24:01 +02:00
jakob.scheid d4814edc65 Add common stylesheets 2026-05-06 22:20:52 +02:00
jakob.scheid 5ba4485bf5 Add main content container 2026-05-06 22:18:16 +02:00
jakob.scheid 6a36b8de68 Remove background 2026-05-06 21:20:32 +02:00
jakob.scheid 8703fe4555 Remove unnecessary with of form 2026-05-06 21:13:21 +02:00
jakob.scheid 59ffc4daff Remove width of the search wrapper 2026-05-06 21:06:58 +02:00
jakob.scheid d5e19b6aee Make component styles scoped 2026-05-06 20:39:11 +02:00
jakob.scheid a4bc7698c7 Add searchbar styles 2026-05-06 20:37:32 +02:00
jakob.scheid 4f8240457c Add header styles 2026-05-06 20:35:27 +02:00
jakob.scheid d454405cb7 Add content background 2026-05-06 20:34:13 +02:00
jakob.scheid 7892fe2ed6 Add searchbar component 2026-05-06 20:29:19 +02:00
jakob.scheid 06e3601d70 Add navbar component 2026-05-06 20:23:27 +02:00
jakob.scheid 2593b65baf Setup Vue router 2026-05-06 20:23:01 +02:00
jakob.scheid c5a06b6ae4 Setup Vue router 2026-05-06 20:20:31 +02:00
jakob.scheid 28c79ac203 Add vue-router dependency 2026-05-06 18:33:01 +02:00
jakob.scheid dd43667bed Update stylesheets directory 2026-05-06 18:17:56 +02:00
jakob.scheid f0c4bb9d06 Add notice 2026-05-06 18:13:04 +02:00
jakob.scheid 63685121f8 Add third-party licenses 2026-05-06 17:27:35 +02:00
jakob.scheid 703c4c1632 Set up NodeJS project 2026-05-06 17:02:34 +02:00
jakob.scheid 87c0e031f1 Add .gitignore 2026-05-06 16:42:40 +02:00
jakob.scheid 7fdcbfe3ee Merge pull request 'New Style' (#24) from feature/reworked-style into main
Reviewed-on: #24
Reviewed-by: Jakob Scheid
2026-05-06 15:51:57 +02:00
jakob.scheid d928f2d25a Remove unnecessary CSS rule 2026-05-06 15:51:21 +02:00
jakob.scheid 35915e8c7f Remove example links 2026-05-06 15:50:35 +02:00
jakob.scheid 01151d476c Remove background 2026-05-06 15:48:35 +02:00
jakob.scheid cc38b00033 Solve merge conflicts 2026-05-06 15:43:31 +02:00
jakob.scheid d50bf475b6 Make CSS border-radius and padding properties values dynamic 2026-05-06 15:17:11 +02:00
johannes.vos 0a28644dd7 Change Shadow 2026-05-06 15:05:12 +02:00
johannes.vos 377aa919cb Add Input Filed Shadow 2026-05-06 15:03:10 +02:00
johannes.vos b43bac4c79 Fix Background 2026-05-06 14:59:54 +02:00
johannes.vos 8d4c54b01b Change Background 2026-05-06 14:52:01 +02:00
johannes.vos 4497396a8f Fix Layout 2026-05-06 14:48:35 +02:00
johannes.vos 5f7838ebbb Fix Gradient 2026-05-06 14:46:55 +02:00
johannes.vos 8d4ef81b0f Move Slogan closer to h1 2026-05-06 14:44:57 +02:00
johannes.vos 4d78a742e1 Make h1 bigger 2026-05-06 14:40:31 +02:00
johannes.vos ddad350e6d Add text decoration underline when hover in navbar 2026-05-06 14:37:12 +02:00
johannes.vos 5696c0f992 Restructe Navbar 2026-05-06 14:35:48 +02:00
johannes.vos 755c297486 Allign Link to end 2026-05-06 14:29:03 +02:00
jakob.scheid 821afc6e7c Merge pull request 'New Style' (#20) from feature/reworked-style into main
Reviewed-on: #20
Reviewed-by: Jakob Scheid
2026-05-06 14:22:47 +02:00
jakob.scheid 45ef5b67f8 Merge branch 'main' into feature/reworked-style 2026-05-06 14:09:23 +02:00
jakob.scheid fd910db9e9 Merge pull request 'Update license to original license text' (#22) from chore/update-license into main
Reviewed-on: #22
Reviewed-by: Johannes D. Vos
2026-05-06 14:08:08 +02:00
jakob.scheid 01631afb73 Make CSS border-radius and padding properties values dynamic 2026-05-06 12:59:08 +02:00
jakob.scheid bac2718663 Update license to original license text 2026-05-05 19:30:50 +02:00
johannes.vos c7ea4d23af Gitea verify test 2026-05-05 19:11:59 +02:00
johannes.vos ceb4f74739 Test signed commit 2026-05-05 19:11:08 +02:00
johannes.vos 991eac065b Test signed commit 2026-05-05 18:31:00 +02:00
johannes.vos 123bd22d07 Revise Requirements 2026-05-04 21:10:23 +02:00
jakob.scheid c6559e478d Make CSS border-radius and padding properties values dynamic 2026-05-04 19:44:15 +02:00
60 changed files with 9560 additions and 190 deletions
+45
View File
@@ -0,0 +1,45 @@
name: Deploy on dev
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy on dev
runs-on: node-minio
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
set -euo pipefail
npm install
- name: Build
run: |
set -euo pipefail
npm run build
- name: Set alias
run: mc alias set minio ${{ vars.S3_DEV_URL }} ${{ secrets.S3_DEV_ACCESS_KEY }} ${{ secrets.S3_DEV_SECRET_KEY }}
- name: Upload build artifacts to S3
run: |
set -euo pipefail
BUCKET=${{ vars.S3_DEV_BUCKET_NAME }}
BUILD_ID=$(date +%s)-$(git rev-parse --short HEAD)
echo "Build ID: $BUILD_ID"
printf "Copying files ... "
mc cp --recursive ./dist/ "minio/$BUCKET/builds/$BUILD_ID/"
echo "done"
printf "Update current build pointer ... "
echo "$BUILD_ID" | mc pipe "minio/$BUCKET/current"
echo "done"
+3
View File
@@ -0,0 +1,3 @@
.vscode/
node_modules/
dist/
+154 -25
View File
@@ -1,3 +1,4 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -6,59 +7,187 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 Seekra
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+8
View File
@@ -0,0 +1,8 @@
Notice
This software is provided "as is", without warranty of any kind.
This software uses the Vue.js framework.
This software makes use of various open-source components.
See THIRD_PARTY_LICENSES for details.
+2716
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
+28
View File
@@ -0,0 +1,28 @@
<!--
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.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Seekra</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
+3091
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"test:run": "vitest run"
},
"dependencies": {
"terser": "^5.47.1",
"vue": "^3.5.32",
"vue-i18n": "^11.4.4",
"vue-router": "^5.0.6"
},
"devDependencies": {
"@vue/test-utils": "^2.4.6",
"@vitejs/plugin-vue": "^6.0.6",
"jsdom": "^29.1.1",
"vite": "^8.0.10",
"vite-plugin-html": "^3.2.2",
"vitest": "^4.1.7"
}
}
View File
+68
View File
@@ -0,0 +1,68 @@
<!--
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 Navbar from './features/nav/components/Navbar.vue';
import Footer from './features/footer/components/Footer.vue';
import { useColorScheme } from './features/colorScheme/composables/useColorScheme';
import { ref, provide, watch } from 'vue';
const { getColorScheme, updateColorScheme } = useColorScheme();
const colorScheme = ref(getColorScheme());
provide('colorScheme', colorScheme);
watch(colorScheme, val => updateColorScheme(val))
</script>
<template>
<div
:style="{ colorScheme: colorScheme === 'auto' ? 'normal' : (colorScheme === 'dark' ? 'dark' : 'light')}"
:class="{ dark: colorScheme === 'dark', 'color-scheme-auto': colorScheme === 'auto' }"
id="app-wrapper"
>
<Navbar />
<router-view class="main-content" />
<Footer />
</div>
</template>
<style scoped>
#app-wrapper {
--main-content-padding-x: 30px;
--main-content-padding-y: 40px;
--main-content-padding: var(--main-content-padding-y) var(--main-content-padding-x);
}
.main-content {
flex-grow: 1;
}
@media (max-width: 48rem) {
#app-wrapper {
--main-content-padding-x: 15px;
}
}
#app-wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--light-bg);
color: var(--dark);
}
</style>
+26
View File
@@ -0,0 +1,26 @@
<!--
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="M4 8 l8 8 l8 -8"
/>
</svg>

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

+19
View File
@@ -0,0 +1,19 @@
<!--
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" width="758.75" height="192.5">
<defs><linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" style="stop-color:#4ba8eb"/><stop offset="100%" style="stop-color:#043485"/></linearGradient></defs>
<path stroke="url(#gradient)" stroke-width="15" fill="none" d=" M94 51 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 1 43.75 43.75 a43.75 43.75 0 0 1 -43.75 43.75 a43.75 43.75 0 0 1 -43.75 -43.75 M138.75 95.25 h43.75 a43.75 43.75 0 0 0 43.75 -43.75 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 v43.75 v43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 0 43.75 -43.75 M270 95.25 h43.75 a43.75 43.75 0 0 0 43.75 -43.75 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 v43.75 v43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 0 43.75 -43.75 M401.25 0 v192.5 m0 -96.125 l87.5 -87.5 m-87.5 87.5 l87.5 87.5 M532.5 192.5 v-192.5 m0 51.25 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 M663.75 50.25 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 v43.75 v43.75 v43.75 m0 -43.75 a43.75 43.75 0 0 1 -43.75 43.75 a43.75 43.75 0 0 1 -43.75 -43.75 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 v50"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File
@@ -0,0 +1,75 @@
<!--
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 Icon from '@/features/icons/components/Icon.vue';
import { inject } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const colorScheme = inject('colorScheme');
const colorSchemeNextMapper = {
'light': 'dark',
'dark': 'auto',
'auto': 'light'
};
const colorSchemeIconMapper = {
'dark': 'crescent-moon',
'light': 'sun',
'auto': 'circle-black-white'
};
const getTooltipTranslation = function (colorScheme) {
return t(`preferences.colorScheme.switch.${colorSchemeNextMapper[colorScheme]}`);
};
</script>
<template>
<button class="color-scheme-button"
@click="colorScheme = colorSchemeNextMapper[colorScheme]"
:aria-label="getTooltipTranslation(colorScheme)"
:title="getTooltipTranslation(colorScheme)"
>
<Icon
:name="colorSchemeIconMapper[colorSchemeNextMapper[colorScheme]]"
size="16"
/>
</button>
</template>
<style scoped>
.color-scheme-button {
background: none;
border: 1.5px solid var(--border);
border-radius: 50%;
width: 36px;
height: 36px;
cursor: pointer;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
color: var(--dark);
}
.color-scheme-button:hover {
background: var(--light-hover);
}
</style>
@@ -0,0 +1,39 @@
/*
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.
*/
export const useColorScheme = function () {
const getColorScheme = function () {
let colorScheme = localStorage.getItem('colorScheme') || 'auto';
if (!(colorScheme === 'dark' || colorScheme === 'light' || colorScheme === 'auto')) {
colorScheme = 'auto';
};
return colorScheme;
};
const updateColorScheme = function (newScheme) {
let actualNewScheme = newScheme;
if (!(actualNewScheme === 'dark' || actualNewScheme === 'light' || actualNewScheme === 'auto')) {
actualNewScheme = 'auto';
};
localStorage.setItem('colorScheme', actualNewScheme);
};
return {
getColorScheme,
updateColorScheme
};
};
+66
View File
@@ -0,0 +1,66 @@
<!--
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 LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const startYear = 2026
const currentYear = new Date().getFullYear()
const copyrightPeriod =
startYear === currentYear
? `${currentYear}`
: `${startYear}-${currentYear}`
</script>
<template>
<footer class="global-footer">
<div class="footer-segment">
<RouterLink to="/settings" class="link">
{{ t('preferences.settings') }}
</RouterLink>
<LanguageSwitchButton />
</div>
<div class="footer-segment">
&copy; {{ copyrightPeriod }} Seekra
</div>
</footer>
</template>
<style scoped>
.global-footer {
--padding-y: 16px;
}
.footer-segment {
display: flex;
justify-content: center;
gap: 32px;
padding: var(--padding-y);
background-color: var(--light-bg);
border-top: 1px solid var(--border);
}
.global-footer a {
color: var(--dark);
}
</style>
@@ -0,0 +1,164 @@
<!--
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 Icon from '@/features/icons/components/Icon.vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { loadLanguage, LANGUAGES_RTL, SUPPORTED_LANGUAGES } from '@/i18n';
const { t, locale } = useI18n();
const isOpen = ref(false);
const languageDropdown = ref(null);
async function selectLanguage(code) {
await loadLanguage(code);
localStorage.setItem('locale', code);
document.documentElement.lang = code;
document.documentElement.dir = LANGUAGES_RTL.includes(code) ? 'rtl' : 'ltr';
close();
};
const close = function () {
document.removeEventListener('click', closeWrapperOnClickOutsite);
isOpen.value = false;
};
const closeWrapperOnClickOutsite = function (e) {
if (languageDropdown.value) {
if (!languageDropdown.value.contains(e.target)) {
close();
};
};
};
const open = function () {
if (!isOpen.value) {
isOpen.value = true;
setTimeout(() => {
document.addEventListener('click', closeWrapperOnClickOutsite);
}, 0);
};
};
</script>
<template>
<div class="language-switch" tabindex="-1">
<button
class="language-button button"
@click="open"
:aria-expanded="isOpen"
aria-haspopup="listbox"
>
<span class="lang-code">{{ t(`preferences.locale.languages.${locale}`) }}</span>
<span class="chevron" :class="{ open: isOpen }">
<Icon name="chevron-down" size="1em" />
</span>
</button>
<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
v-for="lang in SUPPORTED_LANGUAGES"
:key="lang"
role="option"
:aria-selected="lang === locale"
:class="{ active: lang === locale }"
@click="selectLanguage(lang)"
>
<span class="lang-label">{{ t(`preferences.locale.languages.${lang}`) }}</span>
</li>
</ul>
</div>
</template>
<style scoped>
.language-switch {
--trigger-padding-y: 4px;
position: relative;
}
.language-button {
display: flex;
align-items: center;
gap: 6px;
background: none;
border: 1px solid var(--border);
border-radius: 6px;
padding: var(--trigger-padding-y) 10px;
cursor: pointer;
color: var(--dark);
}
.language-button:hover {
background-color: var(--light-hover);
}
.chevron {
font-size: 0.75rem;
transition: transform 0.2s;
display: inline-block;
}
.chevron.open {
transform: rotate(180deg);
}
.language-dropdown {
--offset: 6px;
position: absolute;
right: 0;
background-color: var(--light-bg);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
list-style: none;
margin: 0;
padding: 6px 0;
min-width: 160px;
z-index: 100;
}
.language-dropdown li {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 16px;
cursor: pointer;
font-size: 0.9rem;
}
.language-dropdown li:hover {
background-color: var(--light-hover);
}
.language-dropdown li.active {
font-weight: bold;
}
.flag {
font-size: 1.1rem;
}
</style>
@@ -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>
+94
View File
@@ -0,0 +1,94 @@
<!--
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 ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
import logo from '@/assets/images/logo.svg';
import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
</script>
<template>
<nav class="global-nav">
<RouterLink to="/" class="link button link">
<img :src="logo" alt="Seekra" class="nav-logo" />
</RouterLink>
<NavbarSearchBarWrapper class="navbar-search-bar-wrapper" />
<ul class="right-links">
<li>
<ColorSchemeButton />
</li>
</ul>
</nav>
<div class="navbar-search-bar-wrapper-small-screens-wrapper">
<NavbarSearchBarWrapper class="navbar-search-bar-wrapper-small-screens" />
</div>
</template>
<style scoped>
.global-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px var(--main-content-padding-x);
height: 42px;
}
.global-nav .right-links {
display: flex;
gap: 30px;
list-style: none;
margin: 0;
padding: 0;
align-items: center;
}
.global-nav .right-links a {
text-decoration: none;
color: var(--dark);
}
.global-nav .right-links a:hover{
text-decoration: underline;
}
.nav-logo {
height: 24px;
width: auto;
}
.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>
@@ -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>
@@ -0,0 +1,117 @@
<!--
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 Icon from '@/features/icons/components/Icon.vue';
const searchQuery = defineModel();
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const router = useRouter()
const props = defineProps(['autoSubmit'])
const submitSearch = function () {
if (props.autoSubmit !== undefined) {
router.push({
name: 'searchResults',
query: { q: searchQuery.value }
});
};
}
</script>
<template>
<div>
<form @submit.prevent="submitSearch" class="search-form">
<div class="search-wrapper">
<input
v-model="searchQuery"
type="search"
:placeholder="t('search.searchBar.placeholder')"
required
/>
<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>
</form>
</div>
</template>
<style scoped>
.search-wrapper {
--submit-button-padding-y: 8px;
--content-height: 32px;
--padding: 4px;
--padding-left: calc(var(--content-height) + var(--padding));
display: flex;
align-items: center;
border: 1.5px solid var(--border);
box-shadow: 0 0px 32px var(--blue-box-shadow);
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y) + var(--padding));
padding: var(--padding);
padding-left: var(--padding-left);
width: 100%;
box-sizing: border-box;
}
.search-wrapper input {
border: none;
outline: none;
width: 100%;
font-size: 1rem;
background: transparent;
height: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
padding-left: 12px;
}
.search-button {
font-size: 1rem;
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: none;
padding: var(--submit-button-padding-y) 20px;
background: var(--primary-color);
color: var(--white);
cursor: pointer;
white-space: nowrap;
display: flex;
justify-content: center;
align-items: center;
}
.search-button .search-icon {
filter: invert(1);
}
.search-button:hover {
background: var(--primary-color-l-1);
}
.search-form {
width: 100%;
}
</style>
@@ -0,0 +1,97 @@
<!--
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 { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
<template>
<div class="main-content-padding">
<div class="search-results-error-message-container">
<div class="search-results-error-message">
<p>{{ t('search.error.searchNotAvailable') }}</p>
<p>{{ t('error.tryAgainToAnotherTime') }}</p>
</div>
</div>
<div class="search-results-container">
</div>
</div>
</template>
<style scoped>
.search-bar {
width: 50%;
}
@media (max-width: 67.5rem) {
.search-bar {
width: 100%;
}
}
.search-results-container {
margin-top: 56px;
}
.search-results-error-message-container {
--error-message-height: 100px;
--error-message-padding: 2em;
display: flex;
justify-content: center;
width: 100%;
}
.search-results-error-message {
padding: var(--error-message-padding);
font-size: 18px;
box-shadow: 0px 0px 42px var(--gray-box-shadow);
border-radius: 28px;
text-align: center;
background-color: var(--light-bg);
animation: fade-in 0.8s ease-out;
height: var(--error-message-height);
display: flex;
justify-content: center;
flex-direction: column;
outline: 1px solid var(--border);
}
@media (max-width: 48rem) {
.search-results-error-message-container {
--error-message-height: 150px;
}
.search-results-error-message {
padding: 1em;
width: 100%;
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
10% {
opacity: 0;
transform: scale(1.02);
}
100% {
opacity: 100;
transform: scale(1);
}
}
</style>
@@ -0,0 +1,48 @@
/*
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 { ref, readonly } from 'vue';
import { loadSettingsConfig } from '../utils/settingsParser.js';
const config = ref(null);
const error = ref(null);
const loading = ref(false);
/**
* Provides reactive access to the parsed settings configuration.
* The config is loaded once and shared across all consumers.
*/
export function useSettingsConfig() {
async function load() {
loading.value = true;
error.value = null;
try {
config.value = await loadSettingsConfig();
} catch (e) {
error.value = e.message;
config.value = null;
} finally {
loading.value = false;
}
}
return {
config: readonly(config),
error: readonly(error),
loading: readonly(loading),
load,
};
}
+3
View File
@@ -0,0 +1,3 @@
{
"contents": []
}
@@ -0,0 +1,65 @@
/*
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.
*/
/**
* @typedef {'bool' | 'number' | 'string' | 'selection' | 'section'} SettingType
*/
/**
* @typedef {Object} SelectionOption
* @property {string} name
* @property {string} i18n
*/
/**
* @typedef {Object} BaseSettingConfig
* @property {SettingType} type
* @property {string} name
* @property {string} i18n
* @property {string} [description]
*/
/**
* @typedef {BaseSettingConfig & {
* default: string | string[],
* allowMultiple: boolean,
* options: SelectionOption[]
* }} SelectionSettingConfig
*/
/**
* @typedef {Object} SectionSettingConfig
* @property {'section'} type
* @property {string} name
* @property {string} [i18n]
* @property {string} [description]
* @property {SettingConfigEntry[]} content
*/
/**
* @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry
*/
/**
* @typedef {{ type: 'bool', name: string, i18n: string, description?: string, default: boolean }} BoolSettingConfig
* @typedef {{ type: 'number', name: string, i18n: string, description?: string, default: number }} NumberSettingConfig
* @typedef {{ type: 'string', name: string, i18n: string, description?: string, default: string }} StringSettingConfig
* @typedef {{ type: 'selection', name: string, i18n: string, description?: string, default: string | string[], allowMultiple?: boolean, options: SelectionOption[] }} SelectionSettingConfig
* @typedef {{ type: 'section', name: string, i18n?: string, description?: string, content: SettingConfigEntry[] }} SectionSettingConfig
* @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry
* @typedef {{ contents: SettingConfigEntry[] }} FirstLevelSettingConfigEntry
* @typedef {{ contents: FirstLevelSettingConfigEntry[] }} SettingsConfig
*/
@@ -0,0 +1,758 @@
/*
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: [
{
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
}
]
}, expected: true },
{ raw: {
contents: [
{
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
]
}, expected: true },
{ raw: {
contents: [
{
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
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: [
{
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
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: 'bool',
name: 'aStandaloneBooleanSetting',
i18n: 'settings.settings.aStandaloneBooleanSetting',
default: true
},
{
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
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 },
{ 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);
});
});
@@ -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.
*/
import { validateSettingsConfig } from './settingsValidator.js';
/**
* Loads and parses the settings configuration via dynamic import.
* @returns {Promise<import('../types/settingsConfig').SettingsConfig>}
*/
export async function loadSettingsConfig() {
let raw;
try {
raw = (await import('../settings.json')).default;
} catch (e) {
throw new Error(`[settings] Failed to load settings.json: ${e.message}`);
}
const result = validateSettingsConfig(raw);
if (!result.valid) {
throw new Error(result.error);
}
return result.config;
}
@@ -0,0 +1,119 @@
/*
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.
*/
const VALID_TYPES = ['bool', 'number', 'string', 'selection', 'section'];
export const assertString = function assertString (value, path) {
if (typeof value !== 'string' || value.trim() === '') {
throw new Error(`[settings] "${path}" must be a non-empty string`);
}
}
export const assertType = function assertType (value, path) {
if (!VALID_TYPES.includes(value)) {
throw new Error(
`[settings] "${path}" has invalid type "${value}". Must be one of: ${VALID_TYPES.join(', ')}`
);
}
}
export const validateSelectionOptions = function validateSelectionOptions (options, path) {
if (!Array.isArray(options) || options.length === 0) {
throw new Error(`[settings] "${path}.options" must be a non-empty array`);
};
options.forEach((opt, i) => {
assertString(opt.name, `${path}.options[${i}].name`);
assertString(opt.i18n, `${path}.options[${i}].i18n`);
});
}
export const validateEntry = function validateEntry (entry, path) {
assertType(entry.type, `${path}.type`);
assertString(entry.name, `${path}.name`);
assertString(entry.i18n, `${path}.i18n`);
if (entry.type === 'section') {
if (!Array.isArray(entry.content)) {
throw new Error(`[settings] "${path}.content" must be an array`);
}
entry.content.forEach((child, i) =>
validateEntry(child, `${path}.content[${i}]`)
);
return;
}
if (entry.type === 'selection') {
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.type === 'bool' && typeof entry.default !== 'boolean') {
throw new Error(`[settings] "${path}.default" must be a boolean`);
}
if (entry.type === 'number' && typeof entry.default !== 'number') {
throw new Error(`[settings] "${path}.default" must be a number`);
}
if (entry.type === 'string' && typeof entry.default !== 'string') {
throw new Error(`[settings] "${path}.default" must be a string`);
}
if (entry.type === 'selection') {
if (typeof entry.default !== 'string') {
throw new Error(`[settings] "${path}.default" must be a string`);
};
if (!entry.options.map((option) => option.name).includes(entry.default)) {
throw new Error(`[settings] option "${path}.default" does not exist`);
};
};
}
}
export const validateFirstLevelSection = function validateFirstLevelSection (section, path) {
assertString(section.name);
assertString(section.i18n);
if (!Array.isArray(section.content)) {
throw new Error(`[settings] "${path}.content" must be an array`);
};
section.content.forEach((entry, i) =>
validateEntry(entry, `${path}.content[${i}]`)
);
};
/**
* Validates a raw settings config object.
* @param {unknown} raw
* @returns {{ valid: true, config: import('../types/settingsConfig').SettingsConfig } | { valid: false, error: string }}
*/
export function validateSettingsConfig(raw) {
try {
if (!raw || typeof raw !== 'object') {
throw new Error('[settings] Config must be an object');
}
if (!Array.isArray(raw.contents)) {
throw new Error('[settings] "contents" must be an array');
}
raw.contents.forEach((entry, i) =>
validateFirstLevelSection(entry, `contents[${i}]`)
);
return { valid: true, config: raw };
} catch (e) {
return { valid: false, error: e.message };
}
}
@@ -0,0 +1,117 @@
<!--
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 LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue';
import { loadSettingsConfig } from '../utils/settingsParser';
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const settingsLoaded = ref(false)
const settings = ref([]);
onMounted(async () => {
settings.value = (await loadSettingsConfig()).contents;
if (!settings.value.map((section) => section.name).includes(getActiveSection())) {
router.push('/settings');
};
settingsLoaded.value = true;
});
const getActiveSection = function getActiveSection () {
const segments = route.path
.split('/')
.filter(Boolean);
return segments[1];
};
</script>
<template>
<div class="settings-page-wrapper">
<header class="header">
<h1>
{{ t('preferences.settings') }}
</h1>
</header>
<LeftSidebarLayout class="layout">
<template #sidebar>
<ul class="sidebar-sections-list">
<li v-for="section in settings">
<RouterLink
:to="`/settings/${section.name}`"
class="button button-link link sidebar-section"
:class="{ active: getActiveSection() === section.name }"
>
{{ t(section.i18n) }}
</RouterLink>
</li>
</ul>
</template>
<div>
<div v-if="!settingsLoaded">
{{ t('loading') }}
</div>
</div>
</LeftSidebarLayout>
</div>
</template>
<style scoped>
.layout {
flex-grow: 1;
}
.settings-page-wrapper {
display: flex;
flex-direction: column;
}
.active {
background-color: var(--light-hover);
}
.header {
padding: var(--main-content-padding-y) var(--main-content-padding-x);
}
.header h1 {
margin: 0;
}
.sidebar-sections-list {
list-style: none;
margin: 0;
padding: 0;
}
.sidebar-section {
--padding: 0.8em;
border-radius: var(--padding);
padding: var(--padding);
margin-bottom: 4px;
width: calc(100% - 2 * var(--padding));
text-align: start;
font-size: 1rem;
display: block;
}
</style>
@@ -0,0 +1,28 @@
<!--
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.
-->
<template>
<nav class="sidebar">
<slot />
</nav>
</template>
<style scoped>
.sidebar {
border-right: 1px solid var(--border);
padding: 20px;
}
</style>
+60
View File
@@ -0,0 +1,60 @@
/*
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 { createI18n } from 'vue-i18n';
import getCurrentLanguage from './utils/currentLanguage';
export const fallbackLocale = 'en';
export const LANGUAGES_RTL = [
'ar'
];
export const SUPPORTED_LANGUAGES = [
'en',
'de',
'fr',
'es',
'it',
'pt'
];
export const i18n = createI18n({
legacy: false,
locale: getCurrentLanguage(),
fallbackLocale: fallbackLocale,
messages: {}
});
const loadedLanguages = new Set();
export async function loadLanguage (locale) {
if (!SUPPORTED_LANGUAGES.includes(locale)) {
locale = fallbackLocale;
}
if (loadedLanguages.has(locale)) {
i18n.global.locale.value = locale;
return;
};
const messages = (await import(`./locales/${locale}.json`)).default;
i18n.global.setLocaleMessage(locale, messages);
i18n.global.locale.value = locale;
loadedLanguages.add(locale);
};
-54
View File
@@ -1,54 +0,0 @@
<!DOCTYPE html>
<!--
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.
-->
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="The Seekra search engine">
<title>Seekra</title>
<link rel="stylesheet" href="static/css/main.css" />
<link rel="stylesheet" href="static/css/nav/navbar/navbar.css" />
</head>
<body>
<nav class="global-nav">
<ul>
<li class="nav-item">
seekra
</li>
</ul>
</nav>
<div class="hero">
<header>
<h1>seekra</h1>
<p>
Built to search.
</p>
</header>
<form id="search-form">
<div class="search-wrapper">
<input type="search" name="search" placeholder="Search..." required />
<button type="submit">Search</button>
</div>
</form>
</div>
<footer>
<p>&copy; 2026 seekra.</p>
</footer>
</body>
</html>
+42
View File
@@ -0,0 +1,42 @@
<!--
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 Sidebar from '@/features/sidebar/components/Sidebar.vue';
</script>
<template>
<div class="layout-container">
<Sidebar>
<slot name="sidebar" />
</Sidebar>
<main class="main-content">
<slot />
</main>
</div>
</template>
<style scoped>
.layout-container {
display: grid;
grid-template-columns: min(24%, 280px) 1fr;
border-top: 1px solid var(--border);
}
.main-content {
padding: 20px;
}
</style>
+42
View File
@@ -0,0 +1,42 @@
{
"loading": "Laden ...",
"search": {
"searchBar": {
"submit": "Suchen",
"placeholder": "Suchen..."
},
"error": {
"searchNotAvailable": "Die Suche ist momentan nicht verfügbar."
}
},
"error": {
"tryAgainToAnotherTime": "Bitte versuchen Sie es zu einem anderen Zeitpunkt erneut.",
"pageNotFound": "Die gesuchte Seite existiert nicht. Bitte überprüfen Sie die URL oder kehren Sie zur Suchseite zurück."
},
"links": {
"back": {
"search": "Zurück zur Suche"
}
},
"preferences": {
"settings": "Einstellungen",
"colorScheme": {
"switch": {
"light": "Zum hellen Modus wechseln",
"dark": "Zum dunklen Modus wechseln",
"auto": "Zum System-Farbschema wechseln"
}
},
"locale": {
"languages": {
"en": "English",
"de": "Deutsch",
"fr": "Français",
"es": "Español",
"it": "Italiano",
"pt": "Português"
}
}
},
"slogan": "Gebaut zum Suchen."
}
+42
View File
@@ -0,0 +1,42 @@
{
"loading": "Loading ...",
"search": {
"searchBar": {
"submit": "Search",
"placeholder": "Search..."
},
"error": {
"searchNotAvailable": "Search is not available right now."
}
},
"error": {
"tryAgainToAnotherTime": "Please try again to another time.",
"pageNotFound": "The page you are looking for does not exist. Please check the URL or return to the search page."
},
"links": {
"back": {
"search": "Back to Search"
}
},
"preferences": {
"settings": "Settings",
"colorScheme": {
"switch": {
"light": "Switch to light mode",
"dark": "Switch to dark mode",
"auto": "Switch to the system color scheme"
}
},
"locale": {
"languages": {
"en": "English",
"de": "Deutsch",
"fr": "Français",
"es": "Español",
"it": "Italiano",
"pt": "Português"
}
}
},
"slogan": "Built to search."
}
+42
View File
@@ -0,0 +1,42 @@
{
"loading": "Cargando ...",
"search": {
"searchBar": {
"submit": "Buscar",
"placeholder": "Buscar..."
},
"error": {
"searchNotAvailable": "La búsqueda no está disponible en este momento."
}
},
"error": {
"tryAgainToAnotherTime": "Por favor, inténtalo de nuevo más tarde.",
"pageNotFound": "La página que buscas no existe. Comprueba la URL o vuelve a la página de búsqueda."
},
"links": {
"back": {
"search": "Volver a la búsqueda"
}
},
"preferences": {
"settings": "Ajustes",
"colorScheme": {
"switch": {
"light": "Cambiar al modo claro",
"dark": "Cambiar al modo oscuro",
"auto": "Usar el esquema de color del sistema"
}
},
"locale": {
"languages": {
"en": "English",
"de": "Deutsch",
"fr": "Français",
"es": "Español",
"it": "Italiano",
"pt": "Português"
}
}
},
"slogan": "Hecho para buscar."
}
+42
View File
@@ -0,0 +1,42 @@
{
"loading": "Chargement ...",
"search": {
"searchBar": {
"submit": "Rechercher",
"placeholder": "Rechercher..."
},
"error": {
"searchNotAvailable": "La recherche n'est pas disponible pour le moment."
}
},
"error": {
"tryAgainToAnotherTime": "Veuillez réessayer ultérieurement.",
"pageNotFound": "La page que vous recherchez n'existe pas. Vérifiez l'URL ou retournez à la page de recherche."
},
"links": {
"back": {
"search": "Retour à la recherche"
}
},
"preferences": {
"settings": "Paramètres",
"colorScheme": {
"switch": {
"light": "Passer en mode clair",
"dark": "Passer en mode sombre",
"auto": "Utiliser le thème système"
}
},
"locale": {
"languages": {
"en": "English",
"de": "Deutsch",
"fr": "Français",
"es": "Español",
"it": "Italiano",
"pt": "Português"
}
}
},
"slogan": "Conçu pour chercher."
}
+42
View File
@@ -0,0 +1,42 @@
{
"loading": "Caricamento ...",
"search": {
"searchBar": {
"submit": "Cerca",
"placeholder": "Cerca..."
},
"error": {
"searchNotAvailable": "La ricerca non è disponibile al momento."
}
},
"error": {
"tryAgainToAnotherTime": "Per favore riprova più tardi.",
"pageNotFound": "La pagina che cerchi non esiste. Controlla l'URL o torna alla pagina di ricerca."
},
"links": {
"back": {
"search": "Torna alla ricerca"
}
},
"preferences": {
"settings": "Impostazioni",
"colorScheme": {
"switch": {
"light": "Passa alla modalità chiara",
"dark": "Passa alla modalità scura",
"auto": "Usa la combinazione colori di sistema"
}
},
"locale": {
"languages": {
"en": "English",
"de": "Deutsch",
"fr": "Français",
"es": "Español",
"it": "Italiano",
"pt": "Português"
}
}
},
"slogan": "Costruito per cercare."
}
+42
View File
@@ -0,0 +1,42 @@
{
"loading": "A carregar ...",
"search": {
"searchBar": {
"submit": "Pesquisar",
"placeholder": "Pesquisar..."
},
"error": {
"searchNotAvailable": "A pesquisa não está disponível no momento."
}
},
"error": {
"tryAgainToAnotherTime": "Por favor, tente novamente mais tarde.",
"pageNotFound": "A página que você procura não existe. Verifique o URL ou volte à página de pesquisa."
},
"links": {
"back": {
"search": "Voltar à pesquisa"
}
},
"preferences": {
"settings": "Configurações",
"colorScheme": {
"switch": {
"light": "Mudar para modo claro",
"dark": "Mudar para modo escuro",
"auto": "Usar esquema de cores do sistema"
}
},
"locale": {
"languages": {
"en": "English",
"de": "Deutsch",
"fr": "Français",
"es": "Español",
"it": "Italiano",
"pt": "Português"
}
}
},
"slogan": "Feito para pesquisar."
}
+30
View File
@@ -0,0 +1,30 @@
/*
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 { createApp } from 'vue'
import App from './App.vue'
import { i18n, loadLanguage } from './i18n';
import getCurrentLanguage from './utils/currentLanguage';
import router from './router'
import './styles/common.css'
import './styles/variables/colors.css'
await loadLanguage(getCurrentLanguage());
createApp(App)
.use(router)
.use(i18n)
.mount('#app')
+94
View File
@@ -0,0 +1,94 @@
/*
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 { createRouter, createWebHistory } from 'vue-router';
import { i18n } from '@/i18n';
import SearchView from '../views/SearchView.vue';
import SearchResultsView from '@/features/search/views/SearchResultsView.vue';
import SettingsView from '@/features/settings/views/SettingsView.vue';
import NotFound from '../views/NotFound.vue';
const routes = [
{
path: '/',
name: 'startPage',
component: SearchView
},
{
path: '/search',
name: 'searchResults',
component: SearchResultsView,
props: route => ({
searchQuery: route.query.q
}),
meta: {
title: (route) => route.query.q
}
},
{
path: '/settings',
name: 'settings',
component: SettingsView,
children: [
{
path: ':rest(.*)',
component: SettingsView
}
],
meta: {
title: () => i18n.global.t('preferences.settings')
}
},
{
path: '/:pathMatch(.*)*',
name: 'notFound',
component: NotFound
},
];
const router = createRouter({
history: createWebHistory(),
routes
});
// remove trailing slash(es)
router.beforeEach((route) => {
if (route.path !== '/' && route.path.endsWith('/')) {
return {
path: route.path.replace(/\/+$/, ''),
query: route.query,
hash: route.hash,
replace: true
};
};
});
// set page title
router.afterEach(to => {
const title =
typeof to.meta.title === 'function'
? to.meta.title(to)
: to.meta.title;
if (title) {
document.title = `${title} - Seekra`;
} else {
document.title = 'Seekra';
};
});
export default router;
-73
View File
@@ -1,73 +0,0 @@
body {
margin: 0;
background-color: #ffffff;
}
.hero {
background:
radial-gradient(ellipse at 40% 60%, rgba(255,255,255,0.55) 0%, transparent 75%),
linear-gradient(160deg, #c8d8f0, #7aa0d8, #5077C7, #dce8f8);
height: 300px;
display: flex;
flex-direction: column;
}
header {
text-align: center;
}
header h1 {
display: inline-block;
margin-bottom: 0;
background: linear-gradient(to right, #689BDB, #5F8DDC, #5077C7, #4562BE, #374FA5, #22298F);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
header p {
margin-top: 0;
font-size: small;
}
form {
display: flex;
justify-content: center;
margin-top: 60px;
width: 100%;
}
.search-wrapper {
display: flex;
align-items: center;
width: 80%;
border: 1.5px solid #ccc;
border-radius: 999px;
padding: 4px 4px 4px 16px;
}
.search-wrapper input {
border: none;
outline: none;
width: 100%;
font-size: 1rem;
background: transparent;
}
.search-wrapper button {
border-radius: 999px;
border: none;
padding: 8px 20px;
background: #4562BE;
color: white;
cursor: pointer;
white-space: nowrap;
}
.search-wrapper button:hover {
background: #374FA5;
}
footer{
text-align: center;
margin-top: 20px;
}
-22
View File
@@ -1,22 +0,0 @@
.global-nav {
display: flex;
justify-content: center;
padding: 10px 0;
}
.global-nav ul {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 30px;
}
.nav-item a {
text-decoration: none;
color: black;
}
.nav-item a:hover {
text-decoration: underline;
}
+64
View File
@@ -0,0 +1,64 @@
/*
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.
*/
body {
margin: 0;
font-size: 16px;
}
.link {
text-decoration: none;
}
.link:hover:not(.button-link), .link:focus:not(.button-link) {
outline: none;
text-decoration: underline;
}
.button-link {
color: var(--dark);
}
.button-link:hover {
color: var(--dark);
}
.button-link:visited {
color: var(--dark);
}
input {
color: var(--dark);
}
.main-content-padding {
padding: var(--main-content-padding);
width: calc(100% - var(--main-content-padding-x) * 2);
}
.button {
background-color: var(--light-bg);
border: none;
}
.button:focus {
background-color: var(--light-hover);
outline: none;
}
.button:hover {
background-color: var(--light-hover);
}
+26
View File
@@ -0,0 +1,26 @@
/*
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.
*/
form {
display: flex;
justify-content: center;
margin-top: 60px;
}
footer{
text-align: center;
margin-top: 20px;
}
+155
View File
@@ -0,0 +1,155 @@
/*
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.
*/
#app-wrapper {
--primary-color-l-8: oklch(0.897 0.202 260);
--primary-color-l-7: oklch(0.847 0.202 260);
--primary-color-l-6: oklch(0.797 0.202 260);
--primary-color-l-6: oklch(0.747 0.202 260);
--primary-color-l-4: oklch(0.697 0.202 260);
--primary-color-l-3: oklch(0.647 0.202 260);
--primary-color-l-2: oklch(0.597 0.202 260);
--primary-color-l-1: oklch(0.547 0.202 260);
--primary-color: oklch(0.497 0.202 260);
--primary-color-d-1: oklch(0.447 0.202 260);
--primary-color-d-2: oklch(0.397 0.202 260);
--primary-color-d-3: oklch(0.347 0.202 260);
--primary-color-d-4: oklch(0.297 0.202 260);
--primary-color-d-5: oklch(0.247 0.202 260);
--primary-color-d-6: oklch(0.197 0.202 260);
--primary-color-d-7: oklch(0.147 0.202 260);
--primary-color-d-8: oklch(0.097 0.202 260);
--black-l-8: oklch(0.4 0 0);
--black-l-7: oklch(0.35 0 0);
--black-l-6: oklch(0.3 0 0);
--black-l-5: oklch(0.25 0 0);
--black-l-4: oklch(0.2 0 0);
--black-l-3: oklch(0.15 0 0);
--black-l-2: oklch(0.1 0 0);
--black-l-1: oklch(0.05 0 0);
--black: oklch(0 0 0);
--white: oklch(1 0 0);
--white-d-1: oklch(0.95 0 0);
--white-d-2: oklch(0.9 0 0);
--white-d-3: oklch(0.85 0 0);
--white-d-4: oklch(0.8 0 0);
--white-d-5: oklch(0.75 0 0);
--white-d-6: oklch(0.7 0 0);
--white-d-7: oklch(0.65 0 0);
--white-d-8: oklch(0.6 0 0);
--dark-l-8: var(--black-l-8);
--dark-l-7: var(--black-l-7);
--dark-l-6: var(--black-l-6);
--dark-l-5: var(--black-l-5);
--dark-l-4: var(--black-l-4);
--dark-l-3: var(--black-l-3);
--dark-l-2: var(--black-l-2);
--dark-l-1: var(--black-l-1);
--dark: var(--black);
--light: var(--white);
--light-d-1: var(--white-d-1);
--light-d-2: var(--white-d-2);
--light-d-3: var(--white-d-3);
--light-d-4: var(--white-d-4);
--light-d-5: var(--white-d-5);
--light-d-6: var(--white-d-6);
--light-d-7: var(--white-d-7);
--light-d-8: var(--white-d-8);
--dark-bg: var(--black-l-2);
--light-bg: var(--white);
--border: var(--white-d-3);
--gray-box-shadow: oklch(0.8 0.0001 271 / 0.7);
--blue-box-shadow: oklch(0.52 0.15 268 / 0.25);
--light-hover: var(--light-d-2);
--invert: invert(0);
}
@media (prefers-color-scheme: dark) {
#app-wrapper.color-scheme-auto {
--dark-l-8: var(--white-d-8);
--dark-l-7: var(--white-d-7);
--dark-l-6: var(--white-d-6);
--dark-l-5: var(--white-d-5);
--dark-l-4: var(--white-d-4);
--dark-l-3: var(--white-d-3);
--dark-l-2: var(--white-d-2);
--dark-l-1: var(--white-d-1);
--dark: var(--white);
--light: var(--black);
--light-d-1: oklch(0.10 0 0);
--light-d-2: var(--black-l-2);
--light-d-3: var(--black-l-3);
--light-d-4: var(--black-l-4);
--light-d-5: var(--black-l-5);
--light-d-6: var(--black-l-6);
--light-d-7: var(--black-l-7);
--light-d-8: var(--black-l-8);
--dark-bg: var(--white);
--light-bg: oklch(0.18 0 0);
--border: var(--black-l-6);
--gray-box-shadow: oklch(0.25 0.0001 271 / 0.7);
--light-hover: var(--light-d-5);
--invert: invert(1);
}
}
#app-wrapper.dark {
--dark-l-8: var(--white-d-8);
--dark-l-7: var(--white-d-7);
--dark-l-6: var(--white-d-6);
--dark-l-5: var(--white-d-5);
--dark-l-4: var(--white-d-4);
--dark-l-3: var(--white-d-3);
--dark-l-2: var(--white-d-2);
--dark-l-1: var(--white-d-1);
--dark: var(--white);
--light: var(--black);
--light-d-1: oklch(0.10 0 0);
--light-d-2: var(--black-l-2);
--light-d-3: var(--black-l-3);
--light-d-4: var(--black-l-4);
--light-d-5: var(--black-l-5);
--light-d-6: var(--black-l-6);
--light-d-7: var(--black-l-7);
--light-d-8: var(--black-l-8);
--dark-bg: var(--white);
--light-bg: oklch(0.18 0 0);
--border: var(--black-l-6);
--gray-box-shadow: oklch(0.25 0.0001 271 / 0.7);
--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);
});
+31
View File
@@ -0,0 +1,31 @@
/*
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.
*/
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;
};
+22
View File
@@ -0,0 +1,22 @@
/*
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.
*/
export default function getCurrentLanguage () {
const saved = localStorage.getItem('locale');
if (saved) return saved;
const locale = new Intl.Locale(navigator.language);
return locale.language;
};
+56
View File
@@ -0,0 +1,56 @@
<!--
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 { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
<template>
<div class="not-found main-content-padding">
<span class="error-message">
{{ t('error.pageNotFound') }}
</span>
<RouterLink to="/" id="link">
{{ t('links.back.search') }}
</RouterLink>
</div>
</template>
<style scoped>
.not-found {
text-align: center;
padding-top: 80px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.error-message{
margin: 0;
font-weight: 600;
font-size: 1.75rem;
}
#link {
align-items: center;
display: flex;
justify-content: center;
margin-top: 20px;
padding: 10px 20px;
text-decoration: none;
}
</style>
+86
View File
@@ -0,0 +1,86 @@
<!--
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 { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import logo from '@/assets/images/logo.svg';
const { t } = useI18n();
const router = useRouter();
const searchQuery = ref('');
const submitSearch = function () {
};
</script>
<template>
<div class="search-content main-content-padding">
<header class="global-header">
<img :src="logo" alt="Seekra" class="header-logo" />
<span class="slogan">
{{ t('slogan') }}
</span>
</header>
<div class="search-container">
<Searchbar v-model="searchQuery" ref="searchbar" class="search-bar" auto-submit />
</div>
</div>
</template>
<style scoped>
.search-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 60px;
}
.global-header {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 40px;
}
.header-logo {
width: 320px;
max-width: 100%;
}
.slogan {
margin-top: 1rem;
font-size: small;
line-height: normal;
}
.search-container {
width: 70%;
max-width: 624px;
}
@media (max-width: 67.5rem) {
.search-container {
width: 100%;
}
}
</style>
+42
View File
@@ -0,0 +1,42 @@
/*
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 { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path';
import { createHtmlPlugin } from 'vite-plugin-html';
// https://vite.dev/config/
export default defineConfig({
build: {
target: 'es2020'
},
plugins: [
vue(),
createHtmlPlugin({
minify: true
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
test: {
globals: false,
environment: 'jsdom'
}
})