From dccdab0d4ac0ed3fada1cdd553192c99ec0861ca Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 5 Aug 2026 13:29:24 +0200 Subject: [PATCH 01/10] feat(legal): add route for the legal notice --- src/features/legal/views/LegalNoticeView.vue | 18 ++++++++++++++++++ src/locales/de.json | 5 +++++ src/locales/en.json | 5 +++++ src/router/index.js | 9 +++++++++ 4 files changed, 37 insertions(+) create mode 100644 src/features/legal/views/LegalNoticeView.vue diff --git a/src/features/legal/views/LegalNoticeView.vue b/src/features/legal/views/LegalNoticeView.vue new file mode 100644 index 0000000..631eaed --- /dev/null +++ b/src/features/legal/views/LegalNoticeView.vue @@ -0,0 +1,18 @@ + + + diff --git a/src/locales/de.json b/src/locales/de.json index 93f7593..0f8134e 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -76,5 +76,10 @@ } } } + }, + "legal": { + "notice": { + "title": "Impressum" + } } } \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index 4c4c8a1..9d0476e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -76,5 +76,10 @@ } } } + }, + "legal": { + "notice": { + "title": "Legal Notice" + } } } \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index 18fe434..28fccfe 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -18,6 +18,7 @@ import { createRouter, createWebHistory } from 'vue-router'; import { i18n } from '@/i18n'; import SearchView from '../views/SearchView.vue'; +import LegalNoticeView from '@/features/legal/views/LegalNoticeView.vue'; import SearchResultsView from '@/features/search/views/SearchResultsView.vue'; import SettingsView from '@/features/settings/views/SettingsView.vue'; import NotFound from '../views/NotFound.vue'; @@ -39,6 +40,14 @@ const routes = [ title: (route) => route.query.q } }, + { + path: '/legal/notice', + name: 'legalNotice', + component: LegalNoticeView, + meta: { + title: () => i18n.global.t('legal.notice.title') + } + }, { path: '/settings/:rest(.*)*', name: 'settings', From 4c39fadd9acad975329c91f203c7a5aa492706a0 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 5 Aug 2026 13:30:40 +0200 Subject: [PATCH 02/10] feat(legal): add legal notice link in the footer Added a link to the legal notice in the footer and revised the layout of the last footer segment: The segment is now a grid container and the legal links (currently only the legal notice link) are displayed at the end. The copyright notice remains centered. On small screens, the legal links are displayed centered below the copyright notice. --- src/features/footer/components/Footer.vue | 43 +++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/features/footer/components/Footer.vue b/src/features/footer/components/Footer.vue index eb1bfda..e67fa8e 100644 --- a/src/features/footer/components/Footer.vue +++ b/src/features/footer/components/Footer.vue @@ -40,8 +40,20 @@ const copyrightPeriod = - diff --git a/src/locales/de.json b/src/locales/de.json index 0f8134e..53e537a 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -79,7 +79,8 @@ }, "legal": { "notice": { - "title": "Impressum" + "title": "Impressum", + "loadError": "Das Impressum konnte nicht geladen werden." } } } \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index 9d0476e..158b349 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -79,7 +79,8 @@ }, "legal": { "notice": { - "title": "Legal Notice" + "title": "Legal Notice", + "loadError": "The legal notice could not be loaded." } } } \ No newline at end of file diff --git a/src/styles/common.css b/src/styles/common.css index 4932a55..2100f66 100644 --- a/src/styles/common.css +++ b/src/styles/common.css @@ -92,4 +92,12 @@ input { /* Temporary until a custom tap highlight is implemented. */ * { -webkit-tap-highlight-color: transparent; -} \ No newline at end of file +} + +.error { + background-color: var(--color-negative-bg); + border: 1px solid var(--color-negative-border); + padding: 1em 2em; + border-radius: 1.2em; + color: var(--white); +} diff --git a/src/styles/variables/colors.css b/src/styles/variables/colors.css index 7e7e71a..bf7f8a0 100644 --- a/src/styles/variables/colors.css +++ b/src/styles/variables/colors.css @@ -84,6 +84,9 @@ body { --light-hover: var(--light-d-2); --invert: invert(0); + + --color-negative-bg: oklch(59% 0.22706 29.244); + --color-negative-border: oklch(51% 0.22706 29.244); } @media (prefers-color-scheme: dark) { From f8d2086433bd6956a4b5b82911238a210dba5414 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 5 Aug 2026 14:40:43 +0200 Subject: [PATCH 06/10] feat(legal): add legal notice loading indicator --- src/features/legal/views/LegalNoticeView.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/features/legal/views/LegalNoticeView.vue b/src/features/legal/views/LegalNoticeView.vue index 889343b..161b9f3 100644 --- a/src/features/legal/views/LegalNoticeView.vue +++ b/src/features/legal/views/LegalNoticeView.vue @@ -21,6 +21,7 @@ import { useI18n } from 'vue-i18n'; const { t } = useI18n(); +const loading = ref(true); const legalNoticeError = ref(null); const legalNoticeHtml = ref(null); onMounted(async () => { @@ -28,6 +29,8 @@ onMounted(async () => { legalNoticeHtml.value = await getLegalNotice(); } catch { legalNoticeError.value = 'legal.notice.loadError'; + } finally { + loading.value = false; } }); @@ -40,7 +43,10 @@ onMounted(async () => {
-
+
+ {{ t('loading') }} +
+
{{ t(legalNoticeError) }}
From f7358ceb346f9158b398b542ab2bdfffd33ae95c Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 5 Aug 2026 15:18:50 +0200 Subject: [PATCH 07/10] test(legal): add tests for getLegalNotice --- .../__tests__/fetchLegalContent.test.js | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/features/legal/services/__tests__/fetchLegalContent.test.js diff --git a/src/features/legal/services/__tests__/fetchLegalContent.test.js b/src/features/legal/services/__tests__/fetchLegalContent.test.js new file mode 100644 index 0000000..4b4604f --- /dev/null +++ b/src/features/legal/services/__tests__/fetchLegalContent.test.js @@ -0,0 +1,110 @@ +/* +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 { FetchError } from '../../errors'; +import { getLegalNotice } from '../fetchLegalContent'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; + +let fetchSpy; + +beforeEach(() => { + fetchSpy = vi.spyOn(globalThis, 'fetch'); +}); + +afterEach(() => { + fetchSpy.mockRestore(); +}); + +const exampleHtml = `

Heading

+

+ ... +

`; + +const exampleHtmlWithLinkClass = `

Heading

+

+ ... +

`; +const exampleHtmlDocument = ` + + Legal Notice + + + ${exampleHtml} + +`; +const exampleHtmlDocumentWithMain = ` + + Legal Notice + + +
+ ${exampleHtml} +
+ +`; + +describe('fetchLegalContent', () => { + describe('getLegalNotice', () => { + describe('success', () => { + test('returns HTML body', async () => { + fetchSpy.mockResolvedValue( + new Response(exampleHtmlDocument) + ); + + expect( + (await getLegalNotice()).trim() + ).toBe(exampleHtmlWithLinkClass); + }); + + test('returns HTML
', async () => { + fetchSpy.mockResolvedValue( + new Response(exampleHtmlDocumentWithMain) + ); + + expect( + (await getLegalNotice()).trim() + ).toBe(exampleHtmlWithLinkClass); + }); + + test('does not fail on success status code', async () => { + fetchSpy.mockResolvedValue( + new Response(null, { status: 206 }) + ); + + await getLegalNotice(); + }); + }); + + describe('error', () => { + test('throws FetchError when fetch fails', async () => { + fetchSpy.mockRejectedValue(new TypeError()); + await expect(getLegalNotice()).rejects.toThrow(FetchError); + }); + + test('throws FetchError on HTTP error', async () => { + fetchSpy.mockResolvedValue( + new Response(null, { status: 404 }) + ) + await expect(getLegalNotice()).rejects.toThrow(FetchError); + }); + + test('does not throw FetchError when fetch throws another error than TypeError', async () => { + fetchSpy.mockRejectedValue(new Error()); + await expect(getLegalNotice()).rejects.toThrow(Error); + }); + }); + }); +}); From f73e2edae7a69c49e73be7df0d119281412f5948 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 5 Aug 2026 15:58:00 +0200 Subject: [PATCH 08/10] test(legal): add test for the legal notice view Added tests for the legal notice view (LegalNoticeView.vue). --- src/features/legal/views/LegalNoticeView.vue | 2 +- .../views/__tests__/LegalNoticeView.test.js | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/features/legal/views/__tests__/LegalNoticeView.test.js diff --git a/src/features/legal/views/LegalNoticeView.vue b/src/features/legal/views/LegalNoticeView.vue index 161b9f3..360a394 100644 --- a/src/features/legal/views/LegalNoticeView.vue +++ b/src/features/legal/views/LegalNoticeView.vue @@ -43,7 +43,7 @@ onMounted(async () => {
-
+
diff --git a/src/features/legal/views/__tests__/LegalNoticeView.test.js b/src/features/legal/views/__tests__/LegalNoticeView.test.js new file mode 100644 index 0000000..745a690 --- /dev/null +++ b/src/features/legal/views/__tests__/LegalNoticeView.test.js @@ -0,0 +1,78 @@ +/* +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 { FetchError } from '../../errors.js'; +import LegalNoticeView from '../LegalNoticeView.vue'; +import { mountComponent } from '@/test-utils/mountComponent'; +import { flushPromises } from '@vue/test-utils'; +import { afterEach, describe, expect, test, vi } from 'vitest'; + +const getLegalNotice = vi.hoisted(() => vi.fn().mockResolvedValue('legal notice')); +vi.mock('../../services/fetchLegalContent', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getLegalNotice + }; +}); + +afterEach(() => getLegalNotice.mockRestore()); + +describe('LegalNoticeView', () => { + test('shows loading indicator', () => { + const wrapper = mountComponent(LegalNoticeView); + + const legalNoticeLoading = wrapper.find('.legal-notice-loading'); + const legalNoticeError = wrapper.find('.error'); + const legalNotice = wrapper.find('.legal-notice'); + + expect(legalNoticeLoading.exists()).toBe(true); + expect(legalNoticeError.exists()).toBe(false); + expect(legalNotice.exists()).toBe(false); + }); + + test('shows error', async () => { + getLegalNotice.mockRejectedValue(new FetchError()); + + const wrapper = mountComponent(LegalNoticeView); + + await flushPromises(); + await wrapper.vm.$nextTick(); + + const legalNoticeLoading = wrapper.find('.legal-notice-loading'); + const legalNoticeError = wrapper.find('.error'); + const legalNotice = wrapper.find('.legal-notice'); + + expect(legalNoticeLoading.exists()).toBe(false); + expect(legalNoticeError.exists()).toBe(true); + expect(legalNotice.exists()).toBe(false); + }); + + test('shows legal notice', async () => { + const wrapper = mountComponent(LegalNoticeView); + + await flushPromises(); + await wrapper.vm.$nextTick(); + + const legalNoticeLoading = wrapper.find('.legal-notice-loading'); + const legalNoticeError = wrapper.find('.error'); + const legalNotice = wrapper.find('.legal-notice'); + + expect(legalNoticeLoading.exists()).toBe(false); + expect(legalNoticeError.exists()).toBe(false); + expect(legalNotice.exists()).toBe(true); + }); +}); From f739676a1aed3b21846a8b32aca97f5bae4d06f2 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Thu, 6 Aug 2026 23:34:34 +0200 Subject: [PATCH 09/10] feat(legal): add