diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e1e3a0..8cb2ecc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Global startup loading modal shown while Matrix session restore is in progress - Global auth middleware for protected routes with restore-aware redirect checks - Unit coverage for restore-state transitions and global auth middleware +- Root landing page on `/` with hero content, feature cards, and a download + call-to-action placeholder +- Dedicated landing logo background assets (`logoBg.svg`) for the root route +- Unit tests for root-page restore and redirect behavior in + `tests/unit/pages/index.spec.ts` ### Changed @@ -74,6 +79,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 size and improve composable reuse - Root route startup behavior now waits for restore completion before login/chat navigation decisions +- Root route now keeps restore-aware `watchEffect` gating while rendering + landing content for unauthenticated users +- Landing copy and i18n keys were aligned with the finalized Issue-41 hero and + feature text +- Landing background logo loading now uses a bundler URL import to avoid + runtime path resolution issues ### Fixed diff --git a/app/assets/logoBg.svg b/app/assets/logoBg.svg new file mode 100644 index 0000000..9001bea --- /dev/null +++ b/app/assets/logoBg.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/composables/useAppI18n.ts b/app/composables/useAppI18n.ts index 357a78e..f648ac8 100644 --- a/app/composables/useAppI18n.ts +++ b/app/composables/useAppI18n.ts @@ -88,6 +88,22 @@ const messages: Record> = { 'spaces.name': 'Space name', 'spaces.createStub': 'Create (stub)', 'spaces.stubInfo': 'Real server-side creation will follow in a later step.', + 'landing.kicker': 'OPEN AND FEDERATED TEAM CHAT', + 'landing.title': 'Own your collaboration with Decentra.', + 'landing.subtitle': 'Keep communication in your control while staying connected through Matrix.', + 'landing.loginCta': 'Sign in', + 'landing.chatCta': 'Open chat', + 'landing.featureOneTitle': 'Federated by default', + 'landing.featureOneText': 'Use Matrix homeservers to connect teams without vendor lock-in.', + 'landing.featureTwoTitle': 'Focus on productive channels', + 'landing.featureTwoText': 'Organize spaces, channels and replies in a clean, fast interface.', + 'landing.featureThreeTitle': 'Built for privacy-minded teams', + 'landing.featureThreeText': 'Decentra keeps your communication choices transparent and portable.', + 'landing.downloadTitle': 'Native app download', + 'landing.downloadPlaceholder': 'Desktop and mobile installers will arrive in a future phase.', + 'landing.downloadCta': 'Download coming soon', + 'landing.downloadIos': 'iOS (soon)', + 'landing.downloadAndroid': 'Android (soon)', 'common.loading': 'Loading...' }, de: { @@ -175,6 +191,22 @@ const messages: Record> = { 'spaces.name': 'Space-Name', 'spaces.createStub': 'Erstellen (Stub)', 'spaces.stubInfo': 'Echte Server-Erstellung folgt spaeter.', + 'landing.kicker': 'OPEN AND FEDERATED TEAM CHAT', + 'landing.title': 'Own your collaboration with Decentra.', + 'landing.subtitle': 'Keep communication in your control while staying connected through Matrix.', + 'landing.loginCta': 'Anmelden', + 'landing.chatCta': 'Chat oeffnen', + 'landing.featureOneTitle': 'Foederiert von Anfang an', + 'landing.featureOneText': 'Nutze Matrix-Homeserver fuer Team-Chats ohne Vendor-Lock-in.', + 'landing.featureTwoTitle': 'Fokus auf produktive Kanaele', + 'landing.featureTwoText': 'Organize spaces, channels and replies in a clean, fast interface.', + 'landing.featureThreeTitle': 'Fuer privacy-orientierte Teams gebaut', + 'landing.featureThreeText': 'Decentra haelt Kommunikationsentscheidungen transparent und portabel.', + 'landing.downloadTitle': 'Native app download', + 'landing.downloadPlaceholder': 'Desktop and mobile installers will arrive in a future phase.', + 'landing.downloadCta': 'Download folgt bald', + 'landing.downloadIos': 'iOS (bald)', + 'landing.downloadAndroid': 'Android (bald)', 'common.loading': 'Lade...' } } diff --git a/app/pages/chat.vue b/app/pages/chat.vue index cbbe673..132be0c 100644 --- a/app/pages/chat.vue +++ b/app/pages/chat.vue @@ -521,7 +521,7 @@ async function onLoadOlder() { function handleLogout() { logout(); - navigateTo("/login"); + navigateTo("/"); } function selectSpace(spaceId: string) { diff --git a/app/pages/index.vue b/app/pages/index.vue index e62c637..0494793 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -1,5 +1,6 @@ diff --git a/app/pages/login.vue b/app/pages/login.vue index 476e3a4..9bba756 100644 --- a/app/pages/login.vue +++ b/app/pages/login.vue @@ -81,6 +81,14 @@ async function handleLogin() { > {{ translateText('auth.signIn') }} + + {{ translateText('layout.homeSpace') }} + diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..d7d348f --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DECENTRA + + + diff --git a/public/logoBg.svg b/public/logoBg.svg new file mode 100644 index 0000000..9001bea --- /dev/null +++ b/public/logoBg.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/unit/pages/index.spec.ts b/tests/unit/pages/index.spec.ts new file mode 100644 index 0000000..5919f01 --- /dev/null +++ b/tests/unit/pages/index.spec.ts @@ -0,0 +1,85 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ref, watchEffect } from 'vue' +import { mount } from '@vue/test-utils' +import IndexPage from '~/pages/index.vue' + +const navigateToMock = vi.fn(async () => undefined) + +const UButtonStub = { + template: '' +} + +const UCardStub = { + template: '
' +} + +describe('index page', () => { + beforeEach(() => { + vi.clearAllMocks() + ;(globalThis as Record).watchEffect = watchEffect + ;(globalThis as Record).navigateTo = navigateToMock + ;(globalThis as Record).useAppI18n = () => ({ + translateText: (key: string) => { + const messages: Record = { + 'common.loading': 'Loading...', + 'landing.kicker': 'OPEN AND FEDERATED TEAM CHAT', + 'landing.title': 'Own your collaboration with Decentra.', + 'landing.subtitle': 'Keep communication in your control while staying connected through Matrix.', + 'landing.loginCta': 'Sign in' + } + return messages[key] ?? key + } + }) + }) + + function mountIndexPage(options: { + isLoggedIn: boolean; + isSessionRestoreFinished: boolean; + }) { + ;(globalThis as Record).useMatrixClient = () => ({ + isLoggedIn: ref(options.isLoggedIn), + isSessionRestoreFinished: ref(options.isSessionRestoreFinished) + }) + + return mount(IndexPage, { + global: { + stubs: { + UButton: UButtonStub, + UCard: UCardStub + } + } + }) + } + + it('shows loading text while restore is running', () => { + const wrapper = mountIndexPage({ + isLoggedIn: false, + isSessionRestoreFinished: false + }) + + wrapper.text().includes('Loading...').should.equal(true) + expect(navigateToMock).not.toHaveBeenCalled() + }) + + it('shows landing content after restore when logged out', () => { + const wrapper = mountIndexPage({ + isLoggedIn: false, + isSessionRestoreFinished: true + }) + + wrapper.text().includes('Own your collaboration with Decentra.') + .should.equal(true) + expect(navigateToMock).not.toHaveBeenCalled() + }) + + it('redirects to chat after restore when logged in', async () => { + mountIndexPage({ + isLoggedIn: true, + isSessionRestoreFinished: true + }) + + await Promise.resolve() + + expect(navigateToMock).toHaveBeenCalledWith('/chat') + }) +})