126 Commits

Author SHA1 Message Date
jakob.scheid 2aa6b3794c feat(legal): create empty loader module 2026-05-29 23:39:25 +02:00
jakob.scheid 36bcf32a05 Move legal modules into a feature directory 2026-05-29 23:35:54 +02:00
jakob.scheid eaa8968f64 Fix formatting in the locale files 2026-05-29 21:03:59 +02:00
johannes.vos 0a36bfe4a5 add languages - titel and placeholder 2026-05-29 19:30:35 +02:00
johannes.vos 085dc065bb add placeholder in privacy 2026-05-29 18:58:08 +02:00
johannes.vos a50531593b add router link to privacy 2026-05-29 18:57:40 +02:00
johannes.vos 09da4c5577 create imprint.vue&privacypolicy.vue in views/legal 2026-05-29 18:48:26 +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 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
38 changed files with 812 additions and 120 deletions
+2 -5
View File
@@ -35,9 +35,7 @@ watch(colorScheme, val => updateColorScheme(val))
> >
<Navbar /> <Navbar />
<div class="main-content"> <router-view class="main-content" />
<router-view />
</div>
<Footer /> <Footer />
</div> </div>
@@ -47,8 +45,7 @@ watch(colorScheme, val => updateColorScheme(val))
.main-content { .main-content {
--main-content-padding-x: 30px; --main-content-padding-x: 30px;
--main-content-padding-y: 40px; --main-content-padding-y: 40px;
padding: var(--main-content-padding-y) var(--main-content-padding-x); --main-content-padding: var(--main-content-padding-y) var(--main-content-padding-x) 0;
width: calc(100% - var(--main-content-padding-x) * 2);
flex-grow: 1; flex-grow: 1;
} }
View File
@@ -1,5 +1,5 @@
<!-- <!--
Copyright [yyyy] [name of copyright owner] Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -52,7 +52,7 @@ const getTooltipTranslation = function (colorScheme) {
<style scoped> <style scoped>
.color-scheme-button { .color-scheme-button {
background: none; background: none;
border: 1.5px solid var(--light-d-3); border: 1.5px solid var(--border);
border-radius: 50%; border-radius: 50%;
width: 36px; width: 36px;
height: 36px; height: 36px;
@@ -65,6 +65,6 @@ const getTooltipTranslation = function (colorScheme) {
} }
.color-scheme-button:hover { .color-scheme-button:hover {
background: var(--light-d-2); background: var(--light-hover);
} }
</style> </style>
+26 -5
View File
@@ -1,5 +1,5 @@
<!-- <!--
Copyright [yyyy] [name of copyright owner] Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -15,6 +15,10 @@ limitations under the License.
--> -->
<script setup> <script setup>
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const startYear = 2026 const startYear = 2026
const currentYear = new Date().getFullYear() const currentYear = new Date().getFullYear()
@@ -28,19 +32,36 @@ const copyrightPeriod =
<template> <template>
<footer class="global-footer"> <footer class="global-footer">
<div class="footer-segment">
<RouterLink to="settings" class="link">
{{ t('preferences.settings') }}
</RouterLink>
</div>
<div class="footer-segment">
<div class="copyright-note"> <div class="copyright-note">
&copy; {{ copyrightPeriod }} Seekra &copy; {{ copyrightPeriod }} Seekra
</div> </div>
</div>
</footer> </footer>
</template> </template>
<style scoped> <style scoped>
.global-footer { .global-footer {
--padding-y: 22px; --padding-y: 16px;
}
.footer-segment {
padding: var(--padding-y);
background-color: var(--light-bg);
border-top: 1px solid var(--border);
}
.global-footer a {
color: var(--dark);
}
.copyright-note {
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: var(--padding-y);
background-color: var(--light-d-1);
margin-top: var(--padding-y);
} }
</style> </style>
@@ -15,69 +15,68 @@ limitations under the License.
--> -->
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { loadLanguage } from '@/i18n'; import { loadLanguage, LANGUAGES_RTL, SUPPORTED_LANGUAGES } from '@/i18n';
const { locale } = useI18n(); const { t, locale } = useI18n();
const isOpen = ref(false); const isOpen = ref(false);
const languageDropdown = ref(null);
const languages = [
{ code: 'en', label: 'English', flag: '🇬🇧' },
{ code: 'de', label: 'Deutsch', flag: '🇩🇪' },
{ code: 'fr', label: 'Français', flag: '🇫🇷' },
{ code: 'es', label: 'Español', flag: '🇪🇸' },
{ code: 'it', label: 'Italiano', flag: '🇮🇹' },
{ code: 'pt', label: 'Português', flag: '🇵🇹' },
];
const currentLanguage = computed(
() => languages.find(l => l.code === locale.value) ?? languages[0]
);
async function selectLanguage(code) { async function selectLanguage(code) {
await loadLanguage(code); await loadLanguage(code);
localStorage.setItem('locale', code); localStorage.setItem('locale', code);
document.documentElement.lang = code; document.documentElement.lang = code;
document.documentElement.dir = code === 'ar' ? 'rtl' : 'ltr'; document.documentElement.dir = LANGUAGES_RTL.includes(code) ? 'rtl' : 'ltr';
close();
};
const close = function () {
document.removeEventListener('click', closeWrapperOnClickOutsite);
isOpen.value = false; isOpen.value = false;
} };
function toggle() { const closeWrapperOnClickOutsite = function (e) {
isOpen.value = !isOpen.value; if (languageDropdown.value) {
} if (!languageDropdown.value.contains(e.target)) {
close();
};
};
};
function closeOnBlur() { const open = function () {
// Kleines Delay damit Click auf Option noch registriert wird if (!isOpen.value) {
setTimeout(() => { isOpen.value = false; }, 150); isOpen.value = true;
} setTimeout(() => {
document.addEventListener('click', closeWrapperOnClickOutsite);
}, 0);
};
};
</script> </script>
<template> <template>
<div class="language-switch" @blur="closeOnBlur" tabindex="-1"> <div class="language-switch" tabindex="-1">
<button <button
class="language-button button" class="language-button button"
@click="toggle" @click="open"
:aria-expanded="isOpen" :aria-expanded="isOpen"
aria-haspopup="listbox" aria-haspopup="listbox"
> >
<span class="flag">{{ currentLanguage.flag }}</span> <span class="lang-code">{{ t(`preferences.locale.languages.${locale}`) }}</span>
<span class="lang-code">{{ currentLanguage.code.toUpperCase() }}</span>
<span class="chevron" :class="{ open: isOpen }"></span> <span class="chevron" :class="{ open: isOpen }"></span>
</button> </button>
<ul v-if="isOpen" class="language-dropdown" role="listbox"> <ul v-if="isOpen" ref="languageDropdown" class="language-dropdown" role="listbox">
<li <li
v-for="lang in languages" v-for="lang in SUPPORTED_LANGUAGES"
:key="lang.code" :key="lang"
role="option" role="option"
:aria-selected="lang.code === locale" :aria-selected="lang === locale"
:class="{ active: lang.code === locale }" :class="{ active: lang === locale }"
@click="selectLanguage(lang.code)" @click="selectLanguage(lang)"
> >
<span class="flag">{{ lang.flag }}</span> <span class="lang-label">{{ t(`preferences.locale.languages.${lang}`) }}</span>
<span class="lang-label">{{ lang.label }}</span>
</li> </li>
</ul> </ul>
</div> </div>
@@ -93,16 +92,15 @@ function closeOnBlur() {
align-items: center; align-items: center;
gap: 6px; gap: 6px;
background: none; background: none;
border: 1px solid var(--dark); border: 1px solid var(--border);
border-radius: 6px; border-radius: 6px;
padding: 4px 10px; padding: 4px 10px;
cursor: pointer; cursor: pointer;
font-size: 0.9rem;
color: var(--dark); color: var(--dark);
} }
.language-button:hover { .language-button:hover {
background-color: var(--light-d-1); background-color: var(--light-hover);
} }
.chevron { .chevron {
@@ -120,7 +118,7 @@ function closeOnBlur() {
right: 0; right: 0;
top: calc(100% + 6px); top: calc(100% + 6px);
background-color: var(--light-bg); background-color: var(--light-bg);
border: 1px solid var(--light-d-1); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
list-style: none; list-style: none;
@@ -140,7 +138,7 @@ function closeOnBlur() {
} }
.language-dropdown li:hover { .language-dropdown li:hover {
background-color: var(--light-d-1); background-color: var(--light-hover);
} }
.language-dropdown li.active { .language-dropdown li.active {
+15
View File
@@ -0,0 +1,15 @@
/*
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.
*/
+15
View File
@@ -0,0 +1,15 @@
/*
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.
*/
@@ -0,0 +1,69 @@
```vue
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
// 1. ALLE Sprachen importieren (Verhindert den ReferenceError)
import de from '@/legal/privacy/de.md?raw';
import en from '@/legal/privacy/en.md?raw';
import fr from '@/legal/privacy/fr.md?raw';
import es from '@/legal/privacy/es.md?raw';
import it from '@/legal/privacy/it.md?raw';
import pt from '@/legal/privacy/pt.md?raw';
const { locale } = useI18n();
const content = computed(() => {
const map = {
de,
en,
fr,
es,
it,
pt
};
// Falls eine Sprache mal nicht existiert, nutzen wir 'de' oder 'en' als Fallback
return map[locale.value] || de;
});
</script>
<template>
<main class="privacy-policy-content main-content-padding">
<h1>{{ $t('legal.privacy.title') }}</h1>
<div class="markdown-body">{{ content }}</div>
</main>
</template>
<style scoped>
.privacy-policy-content {
max-width: 900px;
margin: 0 auto;
padding-top: 40px;
padding-bottom: 40px;
}
/* Sorgt dafür, dass die Zeilenumbrüche aus den .md Dateien erhalten bleiben */
.markdown-body {
white-space: pre-wrap;
font-family: inherit;
line-height: 1.6;
}
</style>
+31 -4
View File
@@ -15,9 +15,25 @@ limitations under the License.
--> -->
<script setup> <script setup>
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue'; import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
import LanguageSwitchButton from '@/features/language/components/LanguageSwitchButton.vue'; import LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
import logo from '@/assets/logo.svg'; import logo from '@/assets/images/logo.svg';
import Searchbar from '@/features/search/components/Searchbar.vue';
const route = useRoute();
const searchQueryModel = defineModel();
watch(() => route.name, name => {
searchQueryModel.value = name === 'searchResults' ? route.query.q || '' : '';
});
watch(() => route.query.q, q => {
if (route.name === 'searchResults') {
searchQueryModel.value = q || '';
}
});
</script> </script>
<template> <template>
@@ -25,6 +41,12 @@ import logo from '@/assets/logo.svg';
<RouterLink to="/" class="link button link"> <RouterLink to="/" class="link button link">
<img :src="logo" alt="Seekra" class="nav-logo" /> <img :src="logo" alt="Seekra" class="nav-logo" />
</RouterLink> </RouterLink>
<Searchbar
v-if="route.name === 'searchResults'"
class="search-bar"
v-model="searchQueryModel"
auto-submit
/>
<ul class="right-links"> <ul class="right-links">
<li> <li>
<LanguageSwitchButton /> <LanguageSwitchButton />
@@ -41,7 +63,8 @@ import logo from '@/assets/logo.svg';
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 10px 40px; padding: 18px 40px;
height: 42px;
} }
.global-nav .right-links { .global-nav .right-links {
@@ -50,11 +73,12 @@ import logo from '@/assets/logo.svg';
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
align-items: center;
} }
.global-nav .right-links a { .global-nav .right-links a {
text-decoration: none; text-decoration: none;
color: #000; color: var(--dark);
} }
.global-nav .right-links a:hover{ .global-nav .right-links a:hover{
@@ -65,4 +89,7 @@ import logo from '@/assets/logo.svg';
height: 24px; height: 24px;
width: auto; width: auto;
} }
.search-bar {
width: 70%;
}
</style> </style>
+5 -3
View File
@@ -36,6 +36,7 @@ const submitSearch = function () {
</script> </script>
<template> <template>
<div>
<form @submit.prevent="submitSearch"> <form @submit.prevent="submitSearch">
<div class="search-wrapper"> <div class="search-wrapper">
<input <input
@@ -47,7 +48,8 @@ const submitSearch = function () {
<button type="submit" class="search-button">{{ t('search.searchBar.submit') }}</button> <button type="submit" class="search-button">{{ t('search.searchBar.submit') }}</button>
</div> </div>
</form> </form>
</template> </div>
</template>
<style scoped> <style scoped>
.search-wrapper { .search-wrapper {
@@ -57,8 +59,8 @@ const submitSearch = function () {
--padding-left: calc(var(--content-height) + var(--padding)); --padding-left: calc(var(--content-height) + var(--padding));
display: flex; display: flex;
align-items: center; align-items: center;
border: 1.5px solid var(--light-d-3); border: 1.5px solid var(--border);
box-shadow: 0 0px 32px rgba(69, 98, 190, 0.25); box-shadow: 0 0px 32px var(--blue-box-shadow);
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y) + var(--padding)); border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y) + var(--padding));
padding: var(--padding); padding: var(--padding);
padding-left: var(--padding-left); padding-left: var(--padding-left);
@@ -1,5 +1,5 @@
<!-- <!--
Copyright [yyyy] [name of copyright owner] Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -15,21 +15,13 @@ limitations under the License.
--> -->
<script setup> <script setup>
import Searchbar from '@/features/search/components/Searchbar.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps(['searchQuery']);
const searchQueryModel = defineModel();
searchQueryModel.value = props.searchQuery;
</script> </script>
<template> <template>
<Searchbar class="search-bar" v-model="searchQueryModel" auto-submit /> <div class="main-content-padding">
<div class="search-results-error-message-container"> <div class="search-results-error-message-container">
<div class="search-results-error-message"> <div class="search-results-error-message">
<p>{{ t('search.error.searchNotAvailable') }}</p> <p>{{ t('search.error.searchNotAvailable') }}</p>
@@ -38,6 +30,7 @@ searchQueryModel.value = props.searchQuery;
</div> </div>
<div class="search-results-container"> <div class="search-results-container">
</div> </div>
</div>
</template> </template>
<style scoped> <style scoped>
@@ -68,16 +61,16 @@ searchQueryModel.value = props.searchQuery;
.search-results-error-message { .search-results-error-message {
padding: var(--error-message-padding); padding: var(--error-message-padding);
font-size: 18px; font-size: 18px;
box-shadow: 0px 0px 42px #cdcdcdb3; box-shadow: 0px 0px 42px var(--gray-box-shadow);
border-radius: 28px; border-radius: 28px;
text-align: center; text-align: center;
background-color: #f7f7f7; background-color: var(--light-bg);
animation: fade-in 0.8s ease-out; animation: fade-in 0.8s ease-out;
height: var(--error-message-height); height: var(--error-message-height);
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
outline: 1px solid var(--light-d-2); outline: 1px solid var(--border);
} }
@media (max-width: 48rem) { @media (max-width: 48rem) {
@@ -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,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.
*/
/**
* @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 {Object} SettingsConfig
* @property {SettingConfigEntry[]} contents
*/
/**
* @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[] }} SettingsConfig
*/
@@ -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,99 @@
/*
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'];
function assertString(value, path) {
if (typeof value !== 'string' || value.trim() === '') {
throw new Error(`[settings] "${path}" must be a non-empty string`);
}
}
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(', ')}`
);
}
}
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`);
});
}
function validateEntry(entry, path) {
assertType(entry.type, `${path}.type`);
if (entry.type === 'section') {
assertString(entry.name, `${path}.name`);
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;
}
assertString(entry.name, `${path}.name`);
assertString(entry.i18n, `${path}.i18n`);
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') {
validateSelectionOptions(entry.options, path);
if (typeof entry.allowMultiple !== 'boolean') {
throw new Error(`[settings] "${path}.allowMultiple" must be a boolean`);
}
}
}
}
/**
* 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) =>
validateEntry(entry, `contents[${i}]`)
);
return { valid: true, config: raw };
} catch (e) {
return { valid: false, error: e.message };
}
}
@@ -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 LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
</script>
<template>
<div class="settings-page-wrapper">
<header class="header">
<h1>
{{ t('preferences.settings') }}
</h1>
</header>
<LeftSidebarLayout class="layout">
<template #sidebar>
</template>
</LeftSidebarLayout>
</div>
</template>
<style scoped>
.layout {
flex-grow: 1;
}
.settings-page-wrapper {
display: flex;
flex-direction: column;
}
.header {
padding: var(--main-content-padding-y) var(--main-content-padding-x);
}
.header h1 {
margin: 0;
}
</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>
+11 -2
View File
@@ -19,8 +19,17 @@ import getCurrentLanguage from './utils/currentLanguage';
export const fallbackLocale = 'en'; export const fallbackLocale = 'en';
const SUPPORTED_LANGUAGES = [
'en', 'de', 'fr', 'es', 'it', 'pt', 'zh', 'ja', 'ar', 'hi' export const LANGUAGES_RTL = [
'ar'
];
export const SUPPORTED_LANGUAGES = [
'en',
'de',
'fr',
'es',
'it',
'pt'
]; ];
export const i18n = createI18n({ export const i18n = createI18n({
+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>
+3
View File
@@ -0,0 +1,3 @@
# Datenschutzerklärung
Hier steht deine Datenschutzerklärung auf Deutsch.
+3
View File
@@ -0,0 +1,3 @@
# Privacy Policy
Here is your privacy policy in English.
+3
View File
@@ -0,0 +1,3 @@
# Política de privacidad
Aquí tienes tu política de privacidad en español.
+3
View File
@@ -0,0 +1,3 @@
# Politique de confidentialité
Voici ta politique de confidentialité en français.
+3
View File
@@ -0,0 +1,3 @@
# Informativa sulla privacy
Qui trovi la tua informativa sulla privacy in italiano.
+3
View File
@@ -0,0 +1,3 @@
# Política de Privacidade
Aqui está a tua Política de Privacidade em português.
+20 -3
View File
@@ -9,8 +9,8 @@
} }
}, },
"error": { "error": {
"tryAgainToAnotherTime": "Bitte versuche es zu einem anderen Zeitpunkt erneut.", "tryAgainToAnotherTime": "Bitte versuchen Sie es zu einem anderen Zeitpunkt erneut.",
"pageNotFound": "Die gesuchte Seite existiert nicht. Bitte überprüfe die URL oder kehre zur Suchseite zurück." "pageNotFound": "Die gesuchte Seite existiert nicht. Bitte überprüfen Sie die URL oder kehren Sie zur Suchseite zurück."
}, },
"links": { "links": {
"back": { "back": {
@@ -18,13 +18,30 @@
} }
}, },
"preferences": { "preferences": {
"settings": "Einstellungen",
"colorScheme": { "colorScheme": {
"switch": { "switch": {
"light": "Zum hellen Modus wechseln", "light": "Zum hellen Modus wechseln",
"dark": "Zum dunklen Modus wechseln", "dark": "Zum dunklen Modus wechseln",
"auto": "Zum System-Farbschema 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." "slogan": "Gebaut zum Suchen.",
"legal": {
"privacy": {
"title": "Datenschutzerklärung"
}
}
} }
+18 -1
View File
@@ -18,13 +18,30 @@
} }
}, },
"preferences": { "preferences": {
"settings": "Settings",
"colorScheme": { "colorScheme": {
"switch": { "switch": {
"light": "Switch to light mode", "light": "Switch to light mode",
"dark": "Switch to dark mode", "dark": "Switch to dark mode",
"auto": "Switch to the system color scheme" "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." "slogan": "Built to search.",
"legal": {
"privacy": {
"title": "Privacy Policy"
}
}
} }
+18 -1
View File
@@ -18,13 +18,30 @@
} }
}, },
"preferences": { "preferences": {
"settings": "Ajustes",
"colorScheme": { "colorScheme": {
"switch": { "switch": {
"light": "Cambiar al modo claro", "light": "Cambiar al modo claro",
"dark": "Cambiar al modo oscuro", "dark": "Cambiar al modo oscuro",
"auto": "Usar el esquema de color del sistema" "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." "slogan": "Hecho para buscar.",
"legal": {
"privacy": {
"title": "Política de Privacidad"
}
}
} }
+18 -1
View File
@@ -18,13 +18,30 @@
} }
}, },
"preferences": { "preferences": {
"settings": "Paramètres",
"colorScheme": { "colorScheme": {
"switch": { "switch": {
"light": "Passer en mode clair", "light": "Passer en mode clair",
"dark": "Passer en mode sombre", "dark": "Passer en mode sombre",
"auto": "Utiliser le thème système" "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." "slogan": "Conçu pour chercher.",
"legal": {
"privacy": {
"title": "Politique de Confidentialité"
}
}
} }
+18 -1
View File
@@ -18,13 +18,30 @@
} }
}, },
"preferences": { "preferences": {
"settings": "Impostazioni",
"colorScheme": { "colorScheme": {
"switch": { "switch": {
"light": "Passa alla modalità chiara", "light": "Passa alla modalità chiara",
"dark": "Passa alla modalità scura", "dark": "Passa alla modalità scura",
"auto": "Usa la combinazione colori di sistema" "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." "slogan": "Costruito per cercare.",
"legal": {
"privacy": {
"title": "Politica di Privacy"
}
}
} }
+18 -1
View File
@@ -18,13 +18,30 @@
} }
}, },
"preferences": { "preferences": {
"settings": "Configurações",
"colorScheme": { "colorScheme": {
"switch": { "switch": {
"light": "Mudar para modo claro", "light": "Mudar para modo claro",
"dark": "Mudar para modo escuro", "dark": "Mudar para modo escuro",
"auto": "Usar esquema de cores do sistema" "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." "slogan": "Feito para pesquisar.",
"legal": {
"privacy": {
"title": "Política de Privacidade"
}
}
} }
+20
View File
@@ -15,10 +15,13 @@ limitations under the License.
*/ */
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import { i18n } from '@/i18n';
import SearchView from '../views/SearchView.vue'; import SearchView from '../views/SearchView.vue';
import SearchResultsView from '@/features/search/views/SearchResultsView.vue'; import SearchResultsView from '@/features/search/views/SearchResultsView.vue';
import SettingsView from '@/features/settings/views/SettingsView.vue';
import NotFound from '../views/NotFound.vue'; import NotFound from '../views/NotFound.vue';
import PrivacyPolicyView from '@/features/legal/views/PrivacyPolicyView.vue';
const routes = [ const routes = [
{ {
@@ -37,11 +40,28 @@ const routes = [
title: (route) => route.query.q title: (route) => route.query.q
} }
}, },
{
path: '/settings',
name: 'settings',
component: SettingsView,
meta: {
title: () => i18n.global.t('preferences.settings')
}
},
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'notFound', name: 'notFound',
component: NotFound component: NotFound
}, },
{
path: '/privacy',
name: 'privacyPolicy',
component: PrivacyPolicyView,
meta: {
title: () => 'Privacy Policy'
}
},
]; ];
const router = createRouter({ const router = createRouter({
+5
View File
@@ -30,3 +30,8 @@ body {
input { input {
color: var(--dark); color: var(--dark);
} }
.main-content-padding {
padding: var(--main-content-padding);
width: calc(100% - var(--main-content-padding-x) * 2);
}
+23 -4
View File
@@ -75,6 +75,13 @@ limitations under the License.
--dark-bg: var(--black-l-2); --dark-bg: var(--black-l-2);
--light-bg: var(--white); --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);
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@@ -90,7 +97,7 @@ limitations under the License.
--dark: var(--white); --dark: var(--white);
--light: var(--black); --light: var(--black);
--light-d-1: var(--black-l-1); --light-d-1: oklch(0.10 0 0);
--light-d-2: var(--black-l-2); --light-d-2: var(--black-l-2);
--light-d-3: var(--black-l-3); --light-d-3: var(--black-l-3);
--light-d-4: var(--black-l-4); --light-d-4: var(--black-l-4);
@@ -100,7 +107,13 @@ limitations under the License.
--light-d-8: var(--black-l-8); --light-d-8: var(--black-l-8);
--dark-bg: var(--white); --dark-bg: var(--white);
--light-bg: var(--black); --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);
} }
} }
@@ -116,7 +129,7 @@ limitations under the License.
--dark: var(--white); --dark: var(--white);
--light: var(--black); --light: var(--black);
--light-d-1: var(--black-l-1); --light-d-1: oklch(0.10 0 0);
--light-d-2: var(--black-l-2); --light-d-2: var(--black-l-2);
--light-d-3: var(--black-l-3); --light-d-3: var(--black-l-3);
--light-d-4: var(--black-l-4); --light-d-4: var(--black-l-4);
@@ -126,5 +139,11 @@ limitations under the License.
--light-d-8: var(--black-l-8); --light-d-8: var(--black-l-8);
--dark-bg: var(--white); --dark-bg: var(--white);
--light-bg: var(--black); --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);
} }
+2 -2
View File
@@ -1,5 +1,5 @@
<!-- <!--
Copyright [yyyy] [name of copyright owner] Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@ const { t } = useI18n();
</script> </script>
<template> <template>
<div class="not-found"> <div class="not-found main-content-padding">
<span class="error-message"> <span class="error-message">
{{ t('error.pageNotFound') }} {{ t('error.pageNotFound') }}
</span> </span>
+2 -2
View File
@@ -19,7 +19,7 @@ import Searchbar from '../features/search/components/Searchbar.vue';
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import logo from '@/assets/logo.svg'; import logo from '@/assets/images/logo.svg';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
@@ -32,7 +32,7 @@ const submitSearch = function () {
</script> </script>
<template> <template>
<div class="search-content"> <div class="search-content main-content-padding">
<header class="global-header"> <header class="global-header">
<img :src="logo" alt="Seekra" class="header-logo" /> <img :src="logo" alt="Seekra" class="header-logo" />
<span class="slogan"> <span class="slogan">