feat(about): add motion/depth WOW effects to the "Über uns" page

- AnimatedNumber: stat tiles count up from 0 when scrolled into view
  instead of appearing static, so "live numbers" actually feels live.
- Pull quote: the story's strongest line is pulled out into its own large
  editorial-style callout between the hero paragraphs and the "Von Tier
  zu Tier" section, giving the wall of text a visual beat. paragraph2
  trimmed (DE+EN) so the line isn't read twice back to back.
- PawPrintTrail: a scattered trail of low-opacity paw prints behind the
  story content, each "stamping" into place via a pure-CSS scroll-driven
  animation (animation-timeline: view(), same technique as the existing
  hero-parallax-icon on the landing page) — no JS, respects
  prefers-reduced-motion.

Fixed a real bug while building the trail: -z-10 on its wrapper had no
local stacking context to be scoped to (the parent had `relative` but no
z-index), so it escaped past the page content and rendered behind the
document background — invisible regardless of opacity. Giving the page's
outer wrapper `z-0` fixes it.

Verified locally via dev server + Chrome DevTools MCP: light + dark theme,
375px mobile, quote/trail/stat-tile rendering all confirmed visually.
This commit is contained in:
admin committed 2026-08-14 19:22:14 +02:00
1 parent 9071bf3c9a
commit 7496d900f3
6 files changed
+159 -5

No files matched your search

+2 -1
View File
@@ -648,7 +648,8 @@
"kicker": "Unsere Geschichte",
"title": "Wie aus einem späten Abend eine echte Herzenssache wurde",
"paragraph1": "Kennst du diese Abende, an denen du im Bett liegst, durch Social Media scrollst und dir nach der zehnten Werbeanzeige und dem hundertsten Lifestyle-Post denkst: Eigentlich will ich nur Bilder von Tieren sehen? Genau an so einem Abend, Ende 2025, ist PawFeed entstanden.",
"paragraph2": "Ich habe mich gefragt, warum es keine Plattform gibt, die sich einzig und allein um das dreht, was uns wirklich Freude macht: unsere Tiere. Egal ob Hund, Katze oder Vogel — sie verdienen einen eigenen Raum, in dem sie nicht Nebendarsteller sind, sondern die Hauptrolle spielen. Also habe ich angefangen, genau diesen Raum zu bauen.",
"paragraph2": "Ich habe mich gefragt, warum es keine Plattform gibt, die sich einzig und allein um das dreht, was uns wirklich Freude macht: unsere Tiere — Hund, Katze oder Vogel. Also habe ich angefangen, genau diesen Raum zu bauen.",
"pullQuote": "Sie verdienen einen eigenen Raum, in dem sie nicht Nebendarsteller sind, sondern die Hauptrolle spielen.",
"sectionHeading": "Von Tier zu Tier, von Herz zu Herz",
"paragraph3": "Bei PawFeed dreht sich alles um echte Geschichten statt erzwungene Trends. Du folgst den Rassen und Tieren, die dir wirklich am Herzen liegen, teilst Adoptionsgeschichten und den ganz normalen Alltag mit deinem Vierbeiner — ganz ohne den Lärm, der uns sonst überall begegnet.",
"paragraph4": "Aus einer späten Idee wurde nach Monaten voller Arbeit und Herzblut eine echte Herzenssache. Am 1. September 2026 startet PawFeed offiziell — und ich freue mich schon riesig darauf, all die wunderbaren Fellnasen und ihre Geschichten kennenzulernen.",
+2 -1
View File
@@ -648,7 +648,8 @@
"kicker": "Our story",
"title": "How a late night turned into something we truly love",
"paragraph1": "Do you know those nights when you're lying in bed, scrolling through social media, and after the tenth ad and the hundredth lifestyle post you think: I just want to see pictures of animals? That's exactly the kind of night, in late 2025, when PawFeed was born.",
"paragraph2": "I asked myself why there wasn't a platform built entirely around what actually brings us joy: our pets. Dog, cat, or bird — they deserve a space of their own, where they're not a supporting act but the main character. So I started building exactly that.",
"paragraph2": "I asked myself why there wasn't a platform built entirely around what actually brings us joy: our pets — dog, cat, or bird. So I started building exactly that.",
"pullQuote": "They deserve a space of their own, where they're not a supporting act but the main character.",
"sectionHeading": "From pet to pet, heart to heart",
"paragraph3": "PawFeed is about real stories, not forced trends. You follow the breeds and animals you truly care about, share adoption stories and everyday moments with your companion — without the noise that follows us everywhere else.",
"paragraph4": "What started as a late-night idea became, after months of work and heart, something I truly care about. PawFeed officially launches on September 1, 2026 — and I can't wait to meet all the wonderful pets and their stories.",
+29
View File
@@ -218,6 +218,31 @@
}
}
/* Über-uns paw print trail — scattered decorative icons behind the story
text (Gitea #23) that "stamp" into place as each one scrolls into view.
Base rule sets the resting rotation/scale so the fallback (no scroll-
timeline support) still looks intentional, not broken. */
.paw-trail-icon {
transform: scale(1) rotate(var(--paw-rot, 0deg));
}
@keyframes paw-trail-reveal {
from {
transform: scale(0.4) rotate(var(--paw-rot, 0deg));
}
to {
transform: scale(1) rotate(var(--paw-rot, 0deg));
}
}
@supports (animation-timeline: view()) {
.paw-trail-icon {
animation: paw-trail-reveal ease-out both;
animation-timeline: view();
animation-range: entry 0% entry 65%;
}
}
@media (prefers-reduced-motion: reduce) {
.scroll-reveal {
opacity: 1;
@@ -228,4 +253,8 @@
.hero-parallax-icon {
animation: none !important;
}
.paw-trail-icon {
animation: none !important;
transform: scale(1) rotate(var(--paw-rot, 0deg)) !important;
}
}
+16 -3
View File
@@ -1,11 +1,13 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { Sparkles, Users, PawPrint, HandHeart } from "lucide-react";
import { Sparkles, Users, PawPrint, HandHeart, Quote } from "lucide-react";
import { createTRPCCaller } from "@/trpc/server";
import { ScrollReveal } from "@/components/landing/ScrollReveal";
import { LandingHeader } from "@/components/landing/LandingHeader";
import { LandingFooter } from "@/components/landing/LandingFooter";
import { PawPrintTrail } from "@/components/landing/PawPrintTrail";
import { AnimatedNumber } from "@/components/landing/AnimatedNumber";
export const metadata: Metadata = {
title: "Über uns — PawFeed",
@@ -29,7 +31,8 @@ export default async function AboutPage() {
] as const;
return (
<div className="min-h-screen bg-background">
<div className="relative z-0 min-h-screen bg-background">
<PawPrintTrail />
<LandingHeader />
{/* Story */}
@@ -50,6 +53,16 @@ export default async function AboutPage() {
</ScrollReveal>
</section>
{/* Pull quote */}
<section className="px-4 pb-16 sm:px-6">
<ScrollReveal className="mx-auto max-w-2xl text-center">
<Quote className="mx-auto h-8 w-8 fill-orange-500 text-orange-500" aria-hidden="true" />
<blockquote className="mt-4 text-2xl font-bold leading-snug tracking-tight text-foreground sm:text-3xl">
{t("pullQuote")}
</blockquote>
</ScrollReveal>
</section>
{/* Section heading + third paragraph */}
<section className="px-4 pb-16 sm:px-6">
<ScrollReveal className="mx-auto max-w-3xl rounded-4xl bg-muted/40 px-6 py-12 text-center sm:px-10 sm:py-14">
@@ -80,7 +93,7 @@ export default async function AboutPage() {
<Icon className="h-5.5 w-5.5" aria-hidden="true" />
</div>
<p className="mt-4 text-4xl font-extrabold tracking-tight text-foreground">
{value.toLocaleString()}
<AnimatedNumber value={value} />
</p>
<p className="mt-1 text-sm font-medium text-muted-foreground">{label}</p>
</div>
+60
View File
@@ -0,0 +1,60 @@
"use client";
import { useEffect, useRef, useState } from "react";
interface AnimatedNumberProps {
value: number;
durationMs?: number;
}
/**
* Counts up from 0 to `value` once it scrolls into view — makes the "Über
* uns" live stats (Gitea #23) actually feel live instead of just claiming
* to be. IntersectionObserver + requestAnimationFrame, same pattern as
* ScrollReveal.tsx.
*/
export function AnimatedNumber({ value, durationMs = 1400 }: AnimatedNumberProps) {
const ref = useRef<HTMLSpanElement>(null);
const [display, setDisplay] = useState(0);
const [started, setStarted] = useState(false);
useEffect(() => {
const node = ref.current;
if (!node) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setStarted(true);
observer.disconnect();
}
},
{ threshold: 0.4 }
);
observer.observe(node);
return () => observer.disconnect();
}, []);
useEffect(() => {
if (!started) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
setDisplay(value);
return;
}
const start = performance.now();
let raf: number;
function tick(now: number) {
const progress = Math.min((now - start) / durationMs, 1);
const eased = 1 - Math.pow(1 - progress, 3); // ease-out-cubic
setDisplay(Math.round(eased * value));
if (progress < 1) raf = requestAnimationFrame(tick);
}
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [started, value, durationMs]);
return <span ref={ref}>{display.toLocaleString()}</span>;
}
+50
View File
@@ -0,0 +1,50 @@
import { PawPrint } from "lucide-react";
type Print = {
top: string;
side: "left" | "right";
offset: string;
size: string;
rotate: number;
opacity: number;
};
// Scattered footprint positions down the "Über uns" page — deliberately
// irregular (varied rotation/offset/size) so it reads as a trail, not a grid.
const PRINTS: Print[] = [
{ top: "4%", side: "right", offset: "6%", size: "h-14 w-14 sm:h-20 sm:w-20", rotate: -18, opacity: 0.07 },
{ top: "16%", side: "left", offset: "4%", size: "h-10 w-10 sm:h-14 sm:w-14", rotate: 12, opacity: 0.08 },
{ top: "27%", side: "right", offset: "10%", size: "h-16 w-16 sm:h-24 sm:w-24", rotate: 8, opacity: 0.06 },
{ top: "40%", side: "left", offset: "8%", size: "h-12 w-12 sm:h-16 sm:w-16", rotate: -10, opacity: 0.07 },
{ top: "53%", side: "right", offset: "5%", size: "h-10 w-10 sm:h-14 sm:w-14", rotate: 20, opacity: 0.08 },
{ top: "66%", side: "left", offset: "12%", size: "h-14 w-14 sm:h-20 sm:w-20", rotate: -6, opacity: 0.06 },
{ top: "80%", side: "right", offset: "8%", size: "h-12 w-12 sm:h-16 sm:w-16", rotate: 15, opacity: 0.07 },
{ top: "92%", side: "left", offset: "6%", size: "h-10 w-10 sm:h-12 sm:w-12", rotate: -14, opacity: 0.06 },
];
/**
* Decorative paw print trail behind the "Über uns" story content (Gitea
* #23) — pure CSS scroll-driven animation (see .paw-trail-icon in
* globals.css), so this stays a Server Component. Each print "stamps"
* into place as it scrolls into view instead of just sitting static.
*/
export function PawPrintTrail() {
return (
<div className="pointer-events-none absolute inset-0 -z-10 overflow-hidden" aria-hidden="true">
{PRINTS.map((print, i) => (
<PawPrint
key={i}
className={`paw-trail-icon absolute text-orange-500 ${print.size}`}
style={{
top: print.top,
[print.side]: print.offset,
opacity: print.opacity,
"--paw-rot": `${print.rotate}deg`,
} as React.CSSProperties}
fill="currentColor"
strokeWidth={0}
/>
))}
</div>
);
}