31 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
26 changed files with 242 additions and 118 deletions
View File
-55
View File
@@ -1,55 +0,0 @@
{
"contents": [
{
"name": "general",
"type": "section",
"i18n": "preferences.settings.sections.general.name",
"description": "preferences.settings.sections.general.description",
"content": [
{
"type": "bool",
"name": "darkMode",
"i18n": "preferences.darkMode",
"default": true
},
{
"type": "selection",
"name": "language",
"i18n": "preferences.settings.language",
"description": "preferences.settings.language.description",
"default": "en",
"allowMultiple": false,
"options": [
{
"name": "en",
"i18n": "preferences.locale.languages.en"
},
{
"name": "de",
"i18n": "preferences.locale.languages.de"
}
]
},
{
"type": "section",
"name": "exampleSection",
"description": "preferences.settings.sections.exampleSection.description",
"content": [
{
"type": "number",
"name": "aNumber",
"i18n": "preferences.settings.sections.exampleSection.content.aNumber",
"default": 42
}
]
}
]
},
{
"name": "exampleStandaloneConfiguration",
"type": "bool",
"i18n": "preferences.settings.exampleStandalone.name",
"default": false
}
]
}
+5 -1
View File
@@ -33,7 +33,7 @@ const copyrightPeriod =
<template>
<footer class="global-footer">
<div class="footer-segment">
<RouterLink to="settings">
<RouterLink to="settings" class="link">
{{ t('preferences.settings') }}
</RouterLink>
</div>
@@ -56,6 +56,10 @@ const copyrightPeriod =
border-top: 1px solid var(--border);
}
.global-footer a {
color: var(--dark);
}
.copyright-note {
display: flex;
justify-content: center;
+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>
+28 -2
View File
@@ -15,9 +15,25 @@ limitations under the License.
-->
<script setup>
import { ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
import LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
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>
<template>
@@ -25,6 +41,12 @@ import logo from '@/assets/images/logo.svg';
<RouterLink to="/" class="link button link">
<img :src="logo" alt="Seekra" class="nav-logo" />
</RouterLink>
<Searchbar
v-if="route.name === 'searchResults'"
class="search-bar"
v-model="searchQueryModel"
auto-submit
/>
<ul class="right-links">
<li>
<LanguageSwitchButton />
@@ -41,7 +63,8 @@ import logo from '@/assets/images/logo.svg';
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 40px;
padding: 18px 40px;
height: 42px;
}
.global-nav .right-links {
@@ -66,4 +89,7 @@ import logo from '@/assets/images/logo.svg';
height: 24px;
width: auto;
}
</style>
.search-bar {
width: 70%;
}
</style>
@@ -15,22 +15,13 @@ limitations under the License.
-->
<script setup>
import Searchbar from '@/features/search/components/Searchbar.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps(['searchQuery']);
const searchQueryModel = defineModel();
searchQueryModel.value = props.searchQuery;
</script>
<template>
<div class="main-content-padding">
<Searchbar class="search-bar" v-model="searchQueryModel" auto-submit />
<div class="search-results-error-message-container">
<div class="search-results-error-message">
<p>{{ t('search.error.searchNotAvailable') }}</p>
@@ -26,11 +26,11 @@ const loading = ref(false);
* The config is loaded once and shared across all consumers.
*/
export function useSettingsConfig() {
async function load(url = '/settings.json') {
async function load() {
loading.value = true;
error.value = null;
try {
config.value = await loadSettingsConfig(url);
config.value = await loadSettingsConfig();
} catch (e) {
error.value = e.message;
config.value = null;
+3
View File
@@ -0,0 +1,3 @@
{
"contents": []
}
+9 -12
View File
@@ -32,18 +32,6 @@ limitations under the License.
* @property {string} [description]
*/
/**
* @typedef {BaseSettingConfig & { default: boolean }} BoolSettingConfig
*/
/**
* @typedef {BaseSettingConfig & { default: number }} NumberSettingConfig
*/
/**
* @typedef {BaseSettingConfig & { default: string }} StringSettingConfig
*/
/**
* @typedef {BaseSettingConfig & {
* default: string | string[],
@@ -68,4 +56,13 @@ limitations under the License.
/**
* @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
*/
@@ -17,21 +17,16 @@ limitations under the License.
import { validateSettingsConfig } from './settingsValidator.js';
/**
* Loads and parses the settings configuration from a JSON file.
* @param {string} [url='/settings.json']
* Loads and parses the settings configuration via dynamic import.
* @returns {Promise<import('../types/settingsConfig').SettingsConfig>}
*/
export async function loadSettingsConfig(url = '/settings.json') {
export async function loadSettingsConfig() {
let raw;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
raw = await response.json();
raw = (await import('../settings.json')).default;
} catch (e) {
throw new Error(`[settings] Failed to load config from "${url}": ${e.message}`);
throw new Error(`[settings] Failed to load settings.json: ${e.message}`);
}
const result = validateSettingsConfig(raw);
@@ -58,21 +58,21 @@ function validateEntry(entry, path) {
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`);
}
}
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`);
}
}
}
}
+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.
+8 -2
View File
@@ -37,5 +37,11 @@
}
}
},
"slogan": "Gebaut zum Suchen."
}
"slogan": "Gebaut zum Suchen.",
"legal": {
"privacy": {
"title": "Datenschutzerklärung"
}
}
}
+8 -2
View File
@@ -37,5 +37,11 @@
}
}
},
"slogan": "Built to search."
}
"slogan": "Built to search.",
"legal": {
"privacy": {
"title": "Privacy Policy"
}
}
}
+8 -2
View File
@@ -37,5 +37,11 @@
}
}
},
"slogan": "Hecho para buscar."
}
"slogan": "Hecho para buscar.",
"legal": {
"privacy": {
"title": "Política de Privacidad"
}
}
}
+8 -2
View File
@@ -37,5 +37,11 @@
}
}
},
"slogan": "Conçu pour chercher."
}
"slogan": "Conçu pour chercher.",
"legal": {
"privacy": {
"title": "Politique de Confidentialité"
}
}
}
+8 -2
View File
@@ -37,5 +37,11 @@
}
}
},
"slogan": "Costruito per cercare."
}
"slogan": "Costruito per cercare.",
"legal": {
"privacy": {
"title": "Politica di Privacy"
}
}
}
+8 -2
View File
@@ -37,5 +37,11 @@
}
}
},
"slogan": "Feito para pesquisar."
}
"slogan": "Feito para pesquisar.",
"legal": {
"privacy": {
"title": "Política de Privacidade"
}
}
}
+11 -1
View File
@@ -21,6 +21,7 @@ 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';
import PrivacyPolicyView from '@/features/legal/views/PrivacyPolicyView.vue';
const routes = [
{
@@ -52,6 +53,15 @@ const routes = [
name: 'notFound',
component: NotFound
},
{
path: '/privacy',
name: 'privacyPolicy',
component: PrivacyPolicyView,
meta: {
title: () => 'Privacy Policy'
}
},
];
const router = createRouter({
@@ -73,4 +83,4 @@ router.afterEach(to => {
};
});
export default router;
export default router;