diff --git a/.gitignore b/.gitignore index 23d3d029..982c70ff 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ e2e/playwright-report e2e/.test-state.json e2e/.env.test frontend/static/config/config.json + +# Playwright MCP +.playwright-mcp diff --git a/CHANGELOG.md b/CHANGELOG.md index b44fba23..bfbce5ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- [home] Replace Navigation grid on homepage with a time-based greeting component that addresses the current user by their identifier (morning/afternoon/evening/night, DE + EN). + +### Changed + +- [global] Replace broken date/ID emojis with Carbon `EventSchedule` and `Identification` icons across admin and feature components (Identifiers, Toggles, LinkPresets, Activities, Jokes, JokeOfTheDay). +- [global] Redesign login screen: add logo, `tilloh.dev` title, and tagline above the login input; left-align branding with the input field; compensate PNG transparent padding via negative margin. +- [global] Hide header and redirect to home route when no valid identifier is set; login input is shown full-screen without navigation elements. +- [global] Replace emoji storage-state indicator (🌐/📴) in header with colored Carbon Cloud/CloudOffline icons displayed inline after the page title. +- [global] Increase header route icon size to `1em` (relative to h2 font-size) and align it using flexbox with `gap: 0.3em`. + +### Fixed + +- [global] Fix GlobalMenu showing apps with inactive toggles: `getToggleValue` was defaulting to `true` when a toggle key was missing or unreachable, causing all apps to appear. Changed default to `false` so apps are only shown when their toggle is explicitly enabled. +- [global] Fix letter-spacing not applying to text elements: Carbon CSS explicitly sets `letter-spacing` via its own tokens on `p`, `h1`, etc., overriding the inherited `body` value. Added `body * { letter-spacing: var(--letter-spacing) !important }` to enforce the global spacing across all child elements. + ### Changed +- [todo] Replace raw view-toggle buttons with Carbon Buttons (ghost/secondary/danger); add vertical divider between view toggles and cleanup button; replace raw mobile-edit button with Carbon ghost Button; edit button now always visible directly before delete button; add subtle dark row background with hover highlight; add `--default_padding` gap between edit and delete buttons; indent category headers by `--default_padding / 2`. - [global] Translate all German source code comments to English across e2e specs, frontend components, and utility files. ### Added diff --git a/e2e/tests/auth/login.spec.ts b/e2e/tests/auth/login.spec.ts index f3278cfd..a3900982 100644 --- a/e2e/tests/auth/login.spec.ts +++ b/e2e/tests/auth/login.spec.ts @@ -46,4 +46,19 @@ test.describe('Login-Gate', () => { await expect(notification).toBeVisible({ timeout: 10_000 }); await expect(passwordInput).toBeVisible(); }); + + test('ungültiger Identifier im localStorage zeigt Login-Gate', async ({ page }) => { + // Set an invalid identifier in localStorage before navigation + await page.goto(FRONTEND_URL); + await page.evaluate(() => + localStorage.setItem('identifier', 'invalid-stale-identifier-xyz'), + ); + await page.reload(); + + // Layout verifies on mount and clears invalid identifier → login gate appears + await expect(page.locator('input[type="password"]')).toBeVisible({ + timeout: 15_000, + }); + await expect(page.locator('main')).not.toBeVisible(); + }); }); diff --git a/frontend/src/lib/components/admin/Activities.svelte b/frontend/src/lib/components/admin/Activities.svelte index f0b9c379..004b7bc4 100644 --- a/frontend/src/lib/components/admin/Activities.svelte +++ b/frontend/src/lib/components/admin/Activities.svelte @@ -5,7 +5,9 @@ import AccordionItem from 'carbon-components-svelte/src/Accordion/AccordionItem.svelte'; import CopyButton from 'carbon-components-svelte/src/CopyButton/CopyButton.svelte'; import InlineNotification from 'carbon-components-svelte/src/Notification/InlineNotification.svelte'; + import EventSchedule from 'carbon-icons-svelte/lib/EventSchedule.svelte'; import FaceActivated from 'carbon-icons-svelte/lib/FaceActivated.svelte'; + import Identification from 'carbon-icons-svelte/lib/Identification.svelte'; import Information from 'carbon-icons-svelte/lib/Information.svelte'; import Link from 'carbon-icons-svelte/lib/Link.svelte'; import User from 'carbon-icons-svelte/lib/User.svelte'; @@ -56,12 +58,8 @@
-

- 📅{new Date(activity.updated).toLocaleString('de-DE')} -

-

- 🆔{activity.id} -

+

{new Date(activity.updated).toLocaleString('de-DE')}

+

{activity.id}

-

- ✨{new Date(identifier.created).toLocaleString('de-DE')} - 📅{new Date(identifier.updated).toLocaleString('de-DE')} -

-

- 🆔{identifier._id} -

+

{new Date(identifier.created).toLocaleString('de-DE')}

+

{new Date(identifier.updated).toLocaleString('de-DE')}

+

{identifier._id}

-

- ✨{formatDate(joke.created)} - 📅{formatDate(joke.updated)} -

-

- 🆔{joke._id} -

+

{formatDate(joke.created)}

+

{formatDate(joke.updated)}

+

{joke._id}

{#if !joke.verified && joke.verified !== undefined} diff --git a/frontend/src/lib/components/admin/LinkPresets.svelte b/frontend/src/lib/components/admin/LinkPresets.svelte index fc07ba50..86a0c146 100644 --- a/frontend/src/lib/components/admin/LinkPresets.svelte +++ b/frontend/src/lib/components/admin/LinkPresets.svelte @@ -6,6 +6,8 @@ import Button from 'carbon-components-svelte/src/Button/Button.svelte'; import CopyButton from 'carbon-components-svelte/src/CopyButton/CopyButton.svelte'; import InlineNotification from 'carbon-components-svelte/src/Notification/InlineNotification.svelte'; + import EventSchedule from 'carbon-icons-svelte/lib/EventSchedule.svelte'; + import Identification from 'carbon-icons-svelte/lib/Identification.svelte'; import Link from 'carbon-icons-svelte/lib/Link.svelte'; import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte'; import User from 'carbon-icons-svelte/lib/User.svelte'; @@ -50,13 +52,9 @@
-

- ✨{new Date(linkPreset.created).toLocaleString('de-DE')} - 📅{new Date(linkPreset.updated).toLocaleString('de-DE')} -

-

- 🆔{linkPreset._id} -

+

{new Date(linkPreset.created).toLocaleString('de-DE')}

+

{new Date(linkPreset.updated).toLocaleString('de-DE')}

+

{linkPreset._id}

-

- ✨{new Date(toggle.created).toLocaleString('de-DE')} - 📅{new Date(toggle.updated).toLocaleString('de-DE')} -

-

- 🆔{toggle._id} -

+

{new Date(toggle.created).toLocaleString('de-DE')}

+

{new Date(toggle.updated).toLocaleString('de-DE')}

+

{toggle._id}

+ // 1. IMPORTS + import { getIdentifier } from '$lib/api/identifiers.api'; + import type { IdentifierDto } from '$lib/types/identifiers.dto'; + import { identifierStore } from '$lib/util/stores/store-identifier'; + import { t } from '$lib/util/translations'; + import { onMount } from 'svelte'; + + // 3. CONST + const VARIANTS = 3; + + // 4. STATE + let identifier = $state(null); + let nameLoaded = $state(false); + + // 5. DERIVED + const timeOfDay = $derived.by(() => { + const hour = new Date().getHours(); + if (hour >= 5 && hour < 12) return 'Morning'; + if (hour >= 12 && hour < 18) return 'Afternoon'; + if (hour >= 18 && hour < 22) return 'Evening'; + return 'Night'; + }); + + const greetingVariant = Math.floor(Math.random() * VARIANTS); + + const greetingHtml = $derived.by(() => { + if (!nameLoaded) return ''; + + const hasName = !!identifier?.name; + const prefix = hasName ? '' : 'Anon'; + const key = + `page.home.greeting${prefix}${timeOfDay}${greetingVariant}` as Parameters< + typeof $t + >[0]; + + if (!hasName) return $t(key); + + const escapedName = identifier!.name + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + const highlighted = `${escapedName}`; + return $t(key, { username: highlighted }); + }); + + // 7. LIFECYCLE + onMount(async () => { + if ($identifierStore) { + try { + const result = await getIdentifier($identifierStore); + if (result?.name) { + identifier = result; + } + } catch { + // fall through to anonymous greeting + } + } + nameLoaded = true; + }); + + +{#if nameLoaded} +
+ +

{@html greetingHtml}

+
+{/if} + + diff --git a/frontend/src/lib/components/home/JokeOfTheDay.svelte b/frontend/src/lib/components/home/JokeOfTheDay.svelte index 9b08206b..0c1ee0e1 100644 --- a/frontend/src/lib/components/home/JokeOfTheDay.svelte +++ b/frontend/src/lib/components/home/JokeOfTheDay.svelte @@ -5,6 +5,7 @@ import { formatDate } from '$lib/util/format'; import { initialized, t } from '$lib/util/translations'; import Tile from 'carbon-components-svelte/src/Tile/Tile.svelte'; + import EventSchedule from 'carbon-icons-svelte/lib/EventSchedule.svelte'; import { onMount } from 'svelte'; // 2. PROPS @@ -25,7 +26,7 @@

{$t('page.home.jokeTitle')}

{joke.text} -

📅{formatDate(joke.created)}

+

{formatDate(joke.created)}

{:else}

{$t('page.home.jokeLoading')}

@@ -66,6 +67,9 @@ } p { + display: flex; + align-items: center; + gap: 0.35rem; font-size: 0.75rem; margin-bottom: 0; margin-top: 0.75rem; diff --git a/frontend/src/lib/components/jokes/Jokes.svelte b/frontend/src/lib/components/jokes/Jokes.svelte index 6600a900..7b1a50fa 100644 --- a/frontend/src/lib/components/jokes/Jokes.svelte +++ b/frontend/src/lib/components/jokes/Jokes.svelte @@ -8,6 +8,7 @@ import Tile from 'carbon-components-svelte/src/Tile/Tile.svelte'; import Add from 'carbon-icons-svelte/lib/Add.svelte'; import CloudDownload from 'carbon-icons-svelte/lib/CloudDownload.svelte'; + import EventSchedule from 'carbon-icons-svelte/lib/EventSchedule.svelte'; import { onMount } from 'svelte'; import CreateJokeOverlay from './CreateJokeOverlay.svelte'; @@ -34,7 +35,7 @@ {#if randomJoke?.created} {randomJoke.text} -

📅{formatDate(randomJoke.created)}

+

{formatDate(randomJoke.created)}

{/if} @@ -85,6 +86,12 @@ padding: 2rem; } + :global(.random_joke p) { + display: flex; + align-items: center; + gap: 0.35rem; + } + :global(#add_joke_button) { position: fixed; bottom: calc(var(--default_padding) + env(safe-area-inset-bottom, 0px)); diff --git a/frontend/src/lib/components/shared/GlobalLogin.svelte b/frontend/src/lib/components/shared/GlobalLogin.svelte index b3c1de45..b2190638 100644 --- a/frontend/src/lib/components/shared/GlobalLogin.svelte +++ b/frontend/src/lib/components/shared/GlobalLogin.svelte @@ -5,8 +5,10 @@ import { identifierStore } from '$lib/util/stores/store-identifier'; import { celebrate } from '$lib/util/stores/stores-global'; import { t } from '$lib/util/translations'; + import Button from 'carbon-components-svelte/src/Button/Button.svelte'; import InlineNotification from 'carbon-components-svelte/src/Notification/InlineNotification.svelte'; import PasswordInput from 'carbon-components-svelte/src/TextInput/PasswordInput.svelte'; + import Login from 'carbon-icons-svelte/lib/Login.svelte'; import { fade } from 'svelte/transition'; // 2. PROPS @@ -14,7 +16,7 @@ token = $bindable(''), isVerified = $bindable(false), isAdminLogin = false, - callback = async () => {} + callback = async () => {}, } = $props(); // 3. STATE @@ -58,15 +60,33 @@
-
- { - if (isEnter(event)) verify(); - }} - /> +
@@ -87,6 +107,8 @@ {/if} diff --git a/frontend/src/lib/components/shared/GlobalMenu.svelte b/frontend/src/lib/components/shared/GlobalMenu.svelte index e8db6567..fa26fcb8 100644 --- a/frontend/src/lib/components/shared/GlobalMenu.svelte +++ b/frontend/src/lib/components/shared/GlobalMenu.svelte @@ -1,5 +1,8 @@