From 0aece179061307922f3f2eb97401f4e9fe782d68 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 5 Aug 2026 13:29:24 +0200 Subject: [PATCH 1/8] 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 1b82c31..111e0aa 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -61,5 +61,10 @@ "ariaLabel": "Wählt die Option {option} aus" } } + }, + "legal": { + "notice": { + "title": "Impressum" + } } } \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index c47a8c1..db158af 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -61,5 +61,10 @@ "ariaLabel": "Selects option {option}" } } + }, + "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', -- 2.39.5 From 7ce1e8e2669e762b0e29ba324beabfe81755f033 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 5 Aug 2026 13:30:40 +0200 Subject: [PATCH 2/8] 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 b397207..8d9b8be 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 111e0aa..bc632a9 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -64,7 +64,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 db158af..6b8dd0e 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -64,7 +64,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) { -- 2.39.5 From 46ba0577a9f2e4149e93ad62fa6333960127a975 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 5 Aug 2026 14:40:43 +0200 Subject: [PATCH 6/8] 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) }}
-- 2.39.5 From b2a1662534a14703961650aba694644fef52d9e9 Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 5 Aug 2026 15:18:50 +0200 Subject: [PATCH 7/8] 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); + }); + }); + }); +}); -- 2.39.5 From cfcc36b9127b5294528937e842e588f4fd9801be Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Wed, 5 Aug 2026 15:58:00 +0200 Subject: [PATCH 8/8] 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); + }); +}); -- 2.39.5