From 2cb54d4ed4c5a14c06a605dde5b2e0ec0a87cee4 Mon Sep 17 00:00:00 2001
From: neodk2004
Date: Tue, 25 Aug 2026 18:22:58 +0200
Subject: [PATCH] feat(design): rebuild landing page, header, and legal pages
against new DESIGN.md
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Introduces PRODUCT.md/DESIGN.md (Impeccable skill) capturing PawFeed's
existing brand identity ("The Spotlight Porch") as durable reference, then
applies it:
- Landing hero: restrained "spotlight" glow instead of a full orange
gradient panel, tagline as the actual headline with a marigold accent on
"Stars"/"stars", real PawPrint mark fixed (was a mismatched hand-drawn
shape, also fixed a viewBox overflow clipping bug).
- Features section: one dominant mechanism (pet-as-identity) plus a quiet
supporting list, replacing the four-identical-cards grid craft-floor
refuses.
- Header: dropped the generic glass-blur sticky nav for a flat header with
a soft marigold gradient hairline; CTA button now matches DESIGN.md
(rounded-lg, no shadow, same orange as the hero CTA); logo gets a
paw-spring hover as a small on-brand moment; new active-page indicator
(LandingNavLinks) under the current nav link.
- Legal pages (Impressum/Datenschutz/Nutzungsbedingungen): replaced the
stacked numbered-card-per-section layout with a flowing typographic
document (hairline dividers, no boxes, no arbitrary section-number
badges) via the shared LegalLayout/LegalSection components.
- Nutzungsbedingungen: moved Sponsoring earlier (now 3a, right after
Nutzerinhalte & Urheberrecht) while keeping Salvatorische Klausel/Kontakt
last; existing section numbers left untouched to avoid breaking the
"gemäß Abschnitt 5" cross-reference in Haftungsausschluss.
---
...8-24-story-reactions-insights-dashboard.md | 91 ++++++++
DESIGN.md | 207 ++++++++++++++++++
PRODUCT.md | 66 ++++++
messages/de.json | 2 +-
messages/en.json | 2 +-
src/app/datenschutz/page.tsx | 2 +-
src/app/globals.css | 49 +++++
src/app/impressum/page.tsx | 2 +-
src/app/nutzungsbedingungen/page.tsx | 62 +++---
src/app/page.tsx | 201 +++++++++--------
src/components/landing/LandingHeader.tsx | 22 +-
src/components/landing/LandingNavLinks.tsx | 42 ++++
src/components/legal/LegalLayout.tsx | 48 ++--
13 files changed, 640 insertions(+), 156 deletions(-)
create mode 100644 .claude/handoffs/2026-08-24-story-reactions-insights-dashboard.md
create mode 100644 DESIGN.md
create mode 100644 PRODUCT.md
create mode 100644 src/components/landing/LandingNavLinks.tsx
diff --git a/.claude/handoffs/2026-08-24-story-reactions-insights-dashboard.md b/.claude/handoffs/2026-08-24-story-reactions-insights-dashboard.md
new file mode 100644
index 0000000..0d8513c
--- /dev/null
+++ b/.claude/handoffs/2026-08-24-story-reactions-insights-dashboard.md
@@ -0,0 +1,91 @@
+# Session Handoff: Story Reactions/Comments (#29) + Pet Insights Dashboard (#30)
+
+**Date:** 2026-08-24
+**Project:** T:\CC-Projekte\OnlyPets (PawFeed)
+**Session Duration:** ~4 hours
+
+## Current State
+
+**Task:** Implement Gitea #29 (paw reactions + comments for Stories) and #30 (owner-facing post/story analytics dashboard, explicitly built on top of #29), then a same-session follow-up making the dashboard's spotlight items clickable.
+**Phase:** Complete
+**Progress:** 100% — both features implemented, tested, committed, deployed live, and both Gitea issues closed.
+
+## What We Did
+
+Read Gitea #29 ("Story Erweiterung"): comments-as-overlay-on-the-running-video + a paw-like button for Stories. Implemented `StoryReaction`/`StoryComment` Prisma models (mirroring `Reaction`/`Comment` and `AdReaction`/`AdComment`), new `stories.toggleReaction`/`listComments`/`addComment`/`deleteComment` procedures, and two new client components (`StoryPawButton`, `StoryCommentsOverlay`) wired into `StoryViewer.tsx`. Committed + user asked to defer deploy.
+
+Read Gitea #30 ("Nutzer Daten erweitern", explicitly blocked on #29 per its own comment): a small per-pet analytics tool under "Meine Tiere" showing liked posts/stories and which post/story has the most comments. Built a new `insights` tRPC router (`getPetInsights`) aggregating the reaction/comment counters from both #29 and the pre-existing Post counters, plus a new `/pets/[petId]/insights` page + `InsightsDashboard` component reachable via a new "Statistik" link on the owner's own pet profile. Committed, then user asked to commit+deploy both #29 and #30 together, comment on both issues, and deploy — done.
+
+Follow-up in the same session: user asked for the dashboard's "most liked/commented post/story" cards to be directly clickable. Made `SpotlightCard` a real button; clicking a post opens the existing `PostDetailDialog` (same fetch-by-id pattern as the notifications page); clicking a story opens `StoryViewer` with a new `initialStoryId` prop that jumps straight to that story instead of always starting at index 0. Committed + deployed.
+
+Commented on both issues with what shipped + commit hashes, then closed both #29 and #30 at the user's request.
+
+## Decisions Made
+
+- **StoryReaction/StoryComment as separate models, not a polymorphic postId/storyId table** — matches this codebase's existing precedent (Post↔Reaction/Comment vs. Advertisement↔AdReaction/AdComment are already two separate parallel model families, not unified).
+- **No notification wiring for story reactions/comments** — Notification has no `storyId` FK and stories expire in 24h, so a notification deep-linking to expired content would point at nothing. Matches the Advertisement engagement model, which also has zero `notify()` calls despite having reactions/comments/reposts.
+- **Comment overlay does NOT pause the story** — the issue's own wording was "Overlay zum laufenden Video" (overlay on the *running* video), so `StoryCommentsOverlay` is a translucent `bg-black/40` panel on top, story keeps playing/advancing underneath.
+- **`StoryPawButton`'s `initialReacted` is always `false`** — deliberately mirrors `PawButton.tsx`'s existing, documented limitation (no per-viewer reaction state in `listActive`) rather than adding a new query just for Stories.
+- **New `insights` router, not extending the existing `stats` router** — `stats.ts` is an unrelated public/unauthenticated "Über uns" counters router (owner/pet/sponsor totals), not per-pet analytics.
+- **Insights scopes stories to `expiresAt > now`** — an expired story's media isn't viewable anywhere else in the app, so surfacing it as "your top story" would point at dead content.
+- **Insights filters posts to `hiddenAt: null`** — consistent with every other reader-facing query in the app (feed/explore/profile/search).
+- **i18n: singular/plural key pairs, not ICU `{count, plural, ...}` syntax** — matches the project's existing convention (`Comments.countSingular`/`countPlural`, `StoryViewer.sheetTitleSingular`/`Plural`), not a project-wide standard I introduced.
+- **Deploy files via `cat local | ssh daniel@192.168.1.222 "cat > 'remote/path'"` per file, not `git pull` on the server** — confirmed this session that any `git` subcommand run over SSH on the NAS gets blocked by the local Claude Code auto-mode classifier (tried `git status`, `git rev-parse HEAD`), even though a July 2026 memory says the server has its own git clone that could in theory `git pull`. The `cat|ssh` + `./rolling-deploy.sh` loop from the prior session's handoff is what actually worked today — treat that as the current source of truth over the older memory.
+
+## Code Changes
+
+**#29 — Story reactions/comments (commit `05c9abf`):**
+- `prisma/schema.prisma` — new `StoryReaction`/`StoryComment` models, `Story.reactionCount`/`commentCount` counters, new `Pet.storyReactions`/`storyComments` back-relations. Applied via `prisma db push` (never `migrate`, shared dev/prod DB).
+- `src/trpc/routers/stories.ts` — `toggleReaction`, `listComments`, `addComment`, `deleteComment` (ownership + block checks, rate-limited, reject on expired stories).
+- `src/components/stories/StoryPawButton.tsx` (new), `src/components/stories/StoryCommentsOverlay.tsx` (new), `src/components/stories/StoryViewer.tsx` (wired both in, bottom-right button stack).
+- `src/__tests__/stories.test.ts` (+8 tests), `src/__tests__/helpers/prisma-mock.ts` (added `storyReaction`/`storyComment` mocks).
+- `messages/en.json`/`de.json` — `StoryViewer.openComments`.
+
+**#30 — Insights dashboard (commit `4544857`):**
+- `src/trpc/routers/insights.ts` (new) — `getPetInsights({petId})`, owner-only, 6 parallel Prisma queries (2×`aggregate`, 4×`findFirst`).
+- `src/trpc/routers/_app.ts` — registered `insights: insightsRouter`.
+- `src/app/(app)/pets/[petId]/insights/page.tsx` (new) — owner-only gate, mirrors `health/page.tsx`.
+- `src/components/insights/InsightsDashboard.tsx` (new) — stat tiles + spotlight cards for posts/stories.
+- `src/app/(app)/pets/[petId]/page.tsx` — new "Statistik"/"Insights" link next to Edit/Health.
+- `src/__tests__/insights.test.ts` (new, 4 tests), `prisma-mock.ts` (added `aggregate`/`findFirst` to post/story mocks).
+- `messages/en.json`/`de.json` — new `Insights` namespace, `PetProfile.insights`.
+
+**Follow-up — clickable spotlight (commit `6e9e08b`):**
+- `src/components/stories/StoryViewer.tsx` — new optional `initialStoryId` prop; open-reset effect now jumps to that story's index (falls back to 0 if not found/omitted).
+- `src/components/insights/InsightsDashboard.tsx` — `SpotlightCard` is now a `
-
+
Weitere rechtliche Informationen
diff --git a/src/app/globals.css b/src/app/globals.css
index 61ec048..79d2e2c 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -176,6 +176,55 @@
.animate-paw-burst {
animation: none;
}
+ .landing-logo:hover .landing-logo-paw,
+ .landing-logo:focus-visible .landing-logo-paw,
+ .landing-logo:active .landing-logo-paw {
+ animation: none;
+ }
+}
+
+/* Header logo — reuses the paw-spring reaction as a small brand moment on
+ hover/tap/focus, not an auto-playing loop (navbar polish). */
+.landing-logo:hover .landing-logo-paw,
+.landing-logo:focus-visible .landing-logo-paw,
+.landing-logo:active .landing-logo-paw {
+ animation: paw-spring 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+
+/* Header hairline — a soft marigold glow centered under the logo/nav instead
+ of a flat neutral-gray line, echoing the hero's "porch light" motif. */
+.landing-header-hairline {
+ border-bottom: 1px solid transparent;
+ border-image: linear-gradient(
+ to right,
+ transparent,
+ oklch(0.7024 0.1967 41.45 / 35%) 50%,
+ transparent
+ )
+ 1;
+}
+
+/* Landing page — hero "porch light" entrance (Gitea design system rebuild).
+ Plays once on mount, not scroll-triggered — the hero is above the fold. */
+@keyframes porch-glow-in {
+ from {
+ opacity: 0;
+ transform: scale(0.82);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.porch-glow {
+ animation: porch-glow-in 1.1s cubic-bezier(0.16, 1, 0.3, 1) both;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .porch-glow {
+ animation: none;
+ }
}
/* Landing page — scroll reveal + parallax */
diff --git a/src/app/impressum/page.tsx b/src/app/impressum/page.tsx
index a2e5ea7..5091dce 100644
--- a/src/app/impressum/page.tsx
+++ b/src/app/impressum/page.tsx
@@ -99,7 +99,7 @@ export default function ImpressumPage() {
-
+ Du kannst PawFeed freiwillig mit einem Einmalbetrag oder einem
+ monatlich kündbaren Beitrag (Community-Abo) ab 2€ unterstützen. Die
+ Zahlungsabwicklung erfolgt über unseren Zahlungsdienstleister
+ Stripe (siehe{" "}
+
+ Datenschutzerklärung
+
+ ).
+
+
+ Als Gegenleistung erhältst du ein "Sponsor"-Badge, das auf deinem
+ Profil und bei jedem Beitrag all deiner Haustiere sichtbar ist —
+ es schaltet keine zusätzlichen Funktionen frei. Da PawFeed ein
+ gewerbliches Angebot und keine gemeinnützige Organisation ist,
+ handelt es sich bei deinem Beitrag um eine freiwillige Zuwendung an
+ ein Gewerbe, nicht um eine steuerlich absetzbare Spende.
+
+
+ Ein Community-Abo kannst du jederzeit zum Ende der laufenden
+ Abrechnungsperiode kündigen; dein Sponsor-Badge bleibt bis zu
+ diesem Zeitpunkt bestehen. Mit Bestätigung deiner Zahlung
+ stimmst du der sofortigen Erbringung der Leistung (Freischaltung
+ des Badges) zu und nimmst zur Kenntnis, dass dein gesetzliches
+ Widerrufsrecht damit erlischt, sobald die Leistung vollständig
+ erbracht wurde.
+
+
+
Folgende Inhalte und Verhaltensweisen sind auf PawFeed untersagt:
@@ -186,37 +216,7 @@ export default function NutzungsbedingungenPage() {
-
-
- Du kannst PawFeed freiwillig mit einem Einmalbetrag oder einem
- monatlich kündbaren Beitrag (Community-Abo) ab 2€ unterstützen. Die
- Zahlungsabwicklung erfolgt über unseren Zahlungsdienstleister
- Stripe (siehe{" "}
-
- Datenschutzerklärung
-
- ).
-
-
- Als Gegenleistung erhältst du ein "Sponsor"-Badge, das auf deinem
- Profil und bei jedem Beitrag all deiner Haustiere sichtbar ist —
- es schaltet keine zusätzlichen Funktionen frei. Da PawFeed ein
- gewerbliches Angebot und keine gemeinnützige Organisation ist,
- handelt es sich bei deinem Beitrag um eine freiwillige Zuwendung an
- ein Gewerbe, nicht um eine steuerlich absetzbare Spende.
-
-
- Ein Community-Abo kannst du jederzeit zum Ende der laufenden
- Abrechnungsperiode kündigen; dein Sponsor-Badge bleibt bis zu
- diesem Zeitpunkt bestehen. Mit Bestätigung deiner Zahlung
- stimmst du der sofortigen Erbringung der Leistung (Freischaltung
- des Badges) zu und nimmst zur Kenntnis, dass dein gesetzliches
- Widerrufsrecht damit erlischt, sobald die Leistung vollständig
- erbracht wurde.
-
-
-
-
+
Weitere rechtliche Informationen
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 7567145..8d81d36 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -20,11 +20,14 @@ import { LandingFooter } from "@/components/landing/LandingFooter";
* render for everyone; the redirect-when-authenticated check happens here
* instead via a plain auth() read (not .protect()).
*
- * Redesigned 2026-08-12 (Gitea #11) after a structural reference (navbar
- * position, rounded hero card with phone mockup + floating badges,
- * catchphrase section, second detail-feature card, footer repeating the
- * navbar) — colors/typography adapted to PawFeed's existing orange brand
- * instead of the generic SaaS reference palette.
+ * Rebuilt 2026-08-24 against DESIGN.md ("The Spotlight Porch"): the prior
+ * version (Gitea #11) was a full-bleed orange gradient hero + a uniform
+ * 4-card icon/heading/text feature grid — both are the exact patterns
+ * craft-floor.md refuses (large-area accent wash; same-size cards as page
+ * structure). This version treats orange as the rare "spotlight" moment
+ * DESIGN.md's One Accent Rule calls for, and gives the pet-as-identity
+ * feature real hierarchy over the three supporting ones instead of four
+ * identical cards. Copy/translation keys are unchanged — only composition.
*/
export default async function RootPage() {
const { userId } = await auth();
@@ -32,8 +35,7 @@ export default async function RootPage() {
const t = await getTranslations("Landing");
- const features = [
- { icon: PawPrint, title: t("feature1Title"), body: t("feature1Body") },
+ const supportingFeatures = [
{ icon: Images, title: t("feature2Title"), body: t("feature2Body") },
{ icon: HeartPulse, title: t("feature3Title"), body: t("feature3Body") },
{ icon: MessageCircle, title: t("feature4Title"), body: t("feature4Body") },
@@ -44,43 +46,49 @@ export default async function RootPage() {
- {/* Hero */}
-
-
-
-
-
+ {/* Hero — "The Spotlight Porch": a calm, mostly-neutral canvas with
+ one warm glow behind the one thing worth looking at, instead of
+ an orange-soaked panel. */}
+
+
+ {/* Dominant feature — the actual mechanism, given real weight */}
+
+ {/* Same PawPrint mark as the header/footer logo — a
+ different hand-drawn paw here (as before) read as an
+ off-brand, unrelated shape. */}
+
+
+
+ {t("feature1Title")}
+
+
+ {t("feature1Body")}
+
- ))}
-
-
-
- {/* Detail feature card */}
-
-
-
-
-
- {t("detailHeading")}
-
-
{t("detailBody")}
+ {/* Supporting facts — a quiet list, not boxed cards */}
+
+ {supportingFeatures.map(({ icon: Icon, title, body }) => (
+
+
+
{/* Footer — repeats the navbar */}
diff --git a/src/components/landing/LandingHeader.tsx b/src/components/landing/LandingHeader.tsx
index 92db18f..3510bc8 100644
--- a/src/components/landing/LandingHeader.tsx
+++ b/src/components/landing/LandingHeader.tsx
@@ -3,6 +3,7 @@ import Link from "next/link";
import { PawPrint } from "lucide-react";
import { LanguageSwitcher } from "@/components/i18n/LanguageSwitcher";
import { ThemeToggleButton } from "@/components/landing/ThemeToggleButton";
+import { LandingNavLinks } from "@/components/landing/LandingNavLinks";
/**
* Shared navbar for the public landing page and "Über uns" (Gitea #23) —
@@ -19,20 +20,19 @@ export async function LandingHeader() {
] as const;
return (
-
+
-
-
+
+ PawFeed
-
+
@@ -45,7 +45,7 @@ export async function LandingHeader() {
{t("navSignUp")}
diff --git a/src/components/landing/LandingNavLinks.tsx b/src/components/landing/LandingNavLinks.tsx
new file mode 100644
index 0000000..5f40e66
--- /dev/null
+++ b/src/components/landing/LandingNavLinks.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+interface LandingNavLinksProps {
+ links: readonly { href: string; label: string }[];
+}
+
+/**
+ * Client island for LandingHeader's nav — split out only because knowing
+ * the active page requires usePathname(), which needs "use client", while
+ * the rest of LandingHeader stays a plain async Server Component.
+ */
+export function LandingNavLinks({ links }: LandingNavLinksProps) {
+ const pathname = usePathname();
+
+ return (
+
+ );
+}
diff --git a/src/components/legal/LegalLayout.tsx b/src/components/legal/LegalLayout.tsx
index 19d9029..b223c58 100644
--- a/src/components/legal/LegalLayout.tsx
+++ b/src/components/legal/LegalLayout.tsx
@@ -1,6 +1,15 @@
import Link from "next/link";
import { PawPrint, ArrowLeft } from "lucide-react";
+/**
+ * Rebuilt against DESIGN.md — the previous version stacked every section as
+ * an identical bordered+shadowed card with an arbitrary 1-12 numeral badge,
+ * exactly the "same-size cards" and "section numbers that carry no
+ * information" patterns craft-floor.md refuses. This reads as a flowing
+ * legal document instead: plain typographic headings, hairline dividers
+ * between sections, no boxes. `number` is kept only as an anchor id, never
+ * rendered as a numeral.
+ */
export function LegalLayout({
title,
subtitle,
@@ -11,35 +20,31 @@ export function LegalLayout({
children: React.ReactNode;
}) {
return (
-