diff --git a/app/src/app.html b/app/src/app.html index a4b21b1..a89d42f 100644 --- a/app/src/app.html +++ b/app/src/app.html @@ -10,6 +10,12 @@ + + + + + + Jörg Lohrer diff --git a/app/src/routes/+layout.svelte b/app/src/routes/+layout.svelte index 1b26365..2f06ac9 100644 --- a/app/src/routes/+layout.svelte +++ b/app/src/routes/+layout.svelte @@ -1,32 +1,135 @@ - - - + + +
{@render children()}
+ + diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index f01e49a..3bd8be9 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -4,9 +4,14 @@ import { loadPostList } from '$lib/nostr/loaders'; import { getProfile } from '$lib/nostr/profileCache'; import { AUTHOR_PUBKEY_HEX } from '$lib/nostr/config'; - import ProfileCard from '$lib/components/ProfileCard.svelte'; import PostCard from '$lib/components/PostCard.svelte'; import LoadingOrError from '$lib/components/LoadingOrError.svelte'; + import SocialIcons from '$lib/components/SocialIcons.svelte'; + + // Lokales Profilbild aus static/ — schneller als der Nostr-kind:0-Roundtrip + // fürs kind:0 -> picture-Feld (URL wäre identisch, aber Netzwerk-Latenz). + const HERO_AVATAR = '/joerg-profil-2024.webp'; + const LATEST_COUNT = 5; let profile: Profile | null = $state(null); let posts: NostrEvent[] = $state([]); @@ -29,24 +34,141 @@ }); $effect(() => { - const name = profile?.display_name ?? profile?.name ?? 'Jörg Lohrer'; + const p = profile; + const name = (p && (p.display_name ?? p.name)) ?? 'Jörg Lohrer'; document.title = `${name} – Blog`; }); + + const displayName = $derived.by(() => { + const p = profile; + return (p && (p.display_name ?? p.name)) ?? 'Jörg Lohrer'; + }); + const avatarSrc = HERO_AVATAR; + const about = $derived.by(() => profile?.about ?? ''); + const website = $derived.by(() => profile?.website ?? ''); + const latest = $derived(posts.slice(0, LATEST_COUNT)); + const hasMore = $derived(posts.length > LATEST_COUNT); - +
+
+ {displayName} + +
+
+

{displayName}

+

+ Hi Willkommen auf meinem Blog + +

+ {#if about} +

{about}

+ {/if} + {#if website} + + {/if} +
+
-

Beiträge

- - - -{#each posts as post (post.id)} - -{/each} +
+

Neueste Beiträge

+ + {#each latest as post (post.id)} + + {/each} + {#if hasMore} + + {/if} +
diff --git a/app/src/routes/archiv/+page.svelte b/app/src/routes/archiv/+page.svelte new file mode 100644 index 0000000..ccfcbb0 --- /dev/null +++ b/app/src/routes/archiv/+page.svelte @@ -0,0 +1,80 @@ + + + + Archiv – Jörg Lohrer + + +

Archiv

+

Alle Beiträge, nach Jahr gruppiert.

+ + + +{#each groupsByYear as group (group.year)} +
+

{group.year}

+ {#each group.posts as post (post.id)} + + {/each} +
+{/each} + + diff --git a/app/src/routes/impressum/+page.svelte b/app/src/routes/impressum/+page.svelte new file mode 100644 index 0000000..e2697f8 --- /dev/null +++ b/app/src/routes/impressum/+page.svelte @@ -0,0 +1,40 @@ + + + + Impressum – Jörg Lohrer + + + +
+ {@html html} +
+ + diff --git a/app/static/.htaccess b/app/static/.htaccess index a94ed23..a99df56 100644 --- a/app/static/.htaccess +++ b/app/static/.htaccess @@ -4,6 +4,13 @@ RewriteEngine On RewriteCond %{HTTPS} !=on RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] +# NIP-05-Verifikation: CORS-Header für .well-known/nostr.json, sonst +# lehnen nostr-clients die verifizierung ab. + + Header set Access-Control-Allow-Origin "*" + Header set Content-Type "application/json" + + # Existierende Datei oder Verzeichnis? Direkt ausliefern. RewriteCond %{REQUEST_FILENAME} -f [OR] RewriteCond %{REQUEST_FILENAME} -d diff --git a/app/static/.well-known/nostr.json b/app/static/.well-known/nostr.json new file mode 100644 index 0000000..2b7a301 --- /dev/null +++ b/app/static/.well-known/nostr.json @@ -0,0 +1,14 @@ +{ + "names": { + "joerglohrer": "4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41" + }, + "relays": { + "4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41": [ + "wss://relay.damus.io", + "wss://nos.lol", + "wss://relay.primal.net", + "wss://relay.tchncs.de", + "wss://relay.edufeed.org" + ] + } +} diff --git a/app/static/android-chrome-192x192.png b/app/static/android-chrome-192x192.png new file mode 100755 index 0000000..bf8f3d8 Binary files /dev/null and b/app/static/android-chrome-192x192.png differ diff --git a/app/static/android-chrome-512x512.png b/app/static/android-chrome-512x512.png new file mode 100755 index 0000000..ec3a77c Binary files /dev/null and b/app/static/android-chrome-512x512.png differ diff --git a/app/static/apple-touch-icon.png b/app/static/apple-touch-icon.png new file mode 100755 index 0000000..af60fcd Binary files /dev/null and b/app/static/apple-touch-icon.png differ diff --git a/app/static/favicon-16x16.png b/app/static/favicon-16x16.png new file mode 100755 index 0000000..7411c58 Binary files /dev/null and b/app/static/favicon-16x16.png differ diff --git a/app/static/favicon-32x32.png b/app/static/favicon-32x32.png new file mode 100755 index 0000000..37aeaa5 Binary files /dev/null and b/app/static/favicon-32x32.png differ diff --git a/app/static/favicon.ico b/app/static/favicon.ico new file mode 100755 index 0000000..58e9669 Binary files /dev/null and b/app/static/favicon.ico differ diff --git a/app/static/joerg-profil-2024.webp b/app/static/joerg-profil-2024.webp new file mode 100644 index 0000000..c025a5f Binary files /dev/null and b/app/static/joerg-profil-2024.webp differ diff --git a/app/tests/e2e/home.test.ts b/app/tests/e2e/home.test.ts index 443ed31..083fe33 100644 --- a/app/tests/e2e/home.test.ts +++ b/app/tests/e2e/home.test.ts @@ -1,8 +1,22 @@ import { expect, test } from '@playwright/test'; -test('Home zeigt Profil und mindestens einen Post', async ({ page }) => { +test('Home zeigt Hero (Name + Avatar) und neueste Beiträge', async ({ page }) => { await page.goto('/'); - await expect(page.getByRole('heading', { level: 1, name: /Beiträge/ })).toBeVisible(); - await expect(page.locator('.profile .name')).toBeVisible({ timeout: 15_000 }); + // Hero: Name als h1 + await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); + // Hero: Avatar (lokaler fallback oder nostr-profil) + await expect(page.locator('.hero .avatar')).toBeVisible({ timeout: 15_000 }); + // Neueste-Beiträge-Sektion + await expect(page.getByRole('heading', { level: 2, name: /Neueste Beiträge/i })).toBeVisible(); + // Mindestens ein Post lädt await expect(page.locator('a.card').first()).toBeVisible({ timeout: 15_000 }); }); + +test('Navigation erreicht Archiv und Impressum', async ({ page }) => { + await page.goto('/'); + await page.getByRole('link', { name: 'Archiv', exact: true }).click(); + await expect(page.getByRole('heading', { level: 1, name: /Archiv/i })).toBeVisible(); + + await page.getByRole('link', { name: 'Impressum', exact: true }).first().click(); + await expect(page.getByRole('heading', { level: 1, name: /Impressum/i })).toBeVisible(); +});