feat(design): rebuild landing page, header, and legal pages against new DESIGN.md
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.
This commit is contained in:
1 parent
6e9e08be70
commit
2cb54d4ed4
13 files changed
+640
-156
No files matched your search
@@ -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 `<button>` (disabled when no item); wired `onClick` on all 4 spotlight cards; added `PostDetailDialog` + conditional `StoryViewer` at the bottom, same fetch-by-id pattern as `notifications/page.tsx`.
|
||||
- `src/app/(app)/pets/[petId]/insights/page.tsx` — passes `petAvatarKey` through (needed by `StoryViewer`).
|
||||
|
||||
**Key code context:** `pets.byId` (owner-only) vs `pets.getProfile` (public-safe) is still the load-bearing distinction elsewhere in the app (see prior handoff) — not touched this session, just noting it's still relevant if similar work comes up.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None outstanding — both issues were explicitly scoped, confirmed working (tsc/lint/vitest clean each round), deployed, and closed at the user's direction.
|
||||
|
||||
## Blockers / Issues
|
||||
|
||||
- **Pre-existing lint debt, not touched:** `react-hooks/set-state-in-effect` on `StoryViewer.tsx` (4 pre-existing occurrences, unchanged) and on the new `InsightsDashboard.tsx:98` (`setSelectedPost(fetchedPost as PostCardPost)` inside a `useEffect`) — verified this is an exact match of the same pattern already present and un-fixed in `src/app/(app)/notifications/page.tsx:61`, so left as-is for consistency rather than silently "fixing" an established codebase pattern mid-feature.
|
||||
- **Claude-in-Chrome still has no `localhost` permission** — same limitation as the 2026-08-23 session; live browser verification was skipped again, relied on `tsc --noEmit` + `eslint` + `vitest run` (full suite green each round) instead.
|
||||
- **`git` over SSH to the NAS is blocked by the local auto-mode classifier** — discovered this session (see Decisions above). Any future deploy should go straight to the `cat|ssh` per-file upload method; don't waste a turn trying `ssh ... git ...` first.
|
||||
- **One unrelated pre-existing flaky test:** `legal.test.ts > legal.publish > throws FORBIDDEN for a non-admin caller` timed out once under full-suite parallel load (Redis connection retry contention, same root cause documented for the `stories.toggleReaction`/`addComment` rate-limited tests). Passes cleanly in isolation. Not caused by this session's changes — didn't touch `legal.ts`/`admin.ts`.
|
||||
- **`glitchtip-issue.txt`** has sat untracked in the repo root since before this session started; deliberately excluded from every commit this session (unrelated to the work, not something to clean up unprompted).
|
||||
|
||||
## Context to Remember
|
||||
|
||||
- Deploy loop used successfully twice this session: local `tsc`/lint/vitest clean → `git commit` + `git push origin main` (Gitea) → upload each changed file individually via `cat "local\path" | ssh daniel@192.168.1.222 "cat > '/Dockers/PawFeed/remote/path'"` (create any new directories first with `ssh ... "mkdir -p '...'"`) → `ssh daniel@192.168.1.222 "cd /Dockers/PawFeed/docker && ./rolling-deploy.sh"` → verify with `curl https://pawfeed.org/api/health` (expect `200`).
|
||||
- Schema changes (this session: `StoryReaction`/`StoryComment`) need **both** `npx prisma db push` locally (applies to the shared live DB immediately) **and** uploading the updated `prisma/schema.prisma` to the server before the rebuild, so the server's bundled Prisma Client also knows about the new models — done both times.
|
||||
- Gitea issue/comment workflow: `tea comments add <n> "<text>" --repo admin/petfeed` (note: `tea comments add`, not `comment add`; and `-c`/`--description` flag doesn't take positional text the way it looks — pass the body as the second positional arg). `tea issue close <n> --repo admin/petfeed` to close.
|
||||
- At the very start of this session, before any PawFeed work, the user ran local `/plugin marketplace add nextlevelbuilder/ui-ux-pro-max-skill` and `/plugin install ui-ux-pro-max@ui-ux-pro-max-skill` — unrelated tooling install, not part of this feature work, just noting it happened this session in case it's relevant context for a future session (new `ui-ux-pro-max:*` skills are now available: banner-design, brand, design, design-system, slides, ui-styling).
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. [ ] None required — #29 and #30 are both shipped, verified live, and closed.
|
||||
2. [ ] If a similar "list of items should be clickable" request comes up elsewhere, the pattern established here (fetch full post by id + `PostDetailDialog`, or `StoryViewer` + `initialStoryId`) is now the reusable building block.
|
||||
3. [ ] If another Gitea issue references stale info about server `git pull` deploys, correct it — that path is currently blocked for this session type; use the `cat|ssh` + `rolling-deploy.sh` loop instead.
|
||||
|
||||
## Files to Review on Resume
|
||||
|
||||
- `src/trpc/routers/stories.ts` — reaction/comment procedures (toggleReaction/listComments/addComment/deleteComment).
|
||||
- `src/trpc/routers/insights.ts` — the aggregation query, only 76 lines, easy to extend if more metrics are requested.
|
||||
- `src/components/insights/InsightsDashboard.tsx` — stat tiles + now-clickable spotlight cards.
|
||||
- `src/components/stories/StoryViewer.tsx` — new `initialStoryId` prop, reusable for any other "open story viewer at a specific story" entry point.
|
||||
- `prisma/schema.prisma` — `StoryReaction`/`StoryComment` models, right after `StoryView`.
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
name: PawFeed
|
||||
description: Where pets are the stars — a warm, ad-free social platform built for the animal, not the algorithm.
|
||||
colors:
|
||||
marigold-orange: "oklch(0.7024 0.1967 41.45)"
|
||||
marigold-orange-hover: "#ea580c"
|
||||
marigold-tint: "#ffedd5"
|
||||
marigold-deep: "#c2410c"
|
||||
porch-white: "oklch(0.9882 0 0)"
|
||||
pure-white: "oklch(1 0 0)"
|
||||
ink: "oklch(0.145 0 0)"
|
||||
hairline-gray: "oklch(0.922 0 0)"
|
||||
whisper-gray: "oklch(0.9529 0 0)"
|
||||
quiet-gray: "oklch(0.556 0 0)"
|
||||
alert-red: "oklch(0.577 0.245 27.325)"
|
||||
typography:
|
||||
display:
|
||||
fontFamily: "Inter, system-ui, sans-serif"
|
||||
fontSize: "1.875rem"
|
||||
fontWeight: 800
|
||||
lineHeight: 1.2
|
||||
letterSpacing: "-0.025em"
|
||||
headline:
|
||||
fontFamily: "Inter, system-ui, sans-serif"
|
||||
fontSize: "1.5rem"
|
||||
fontWeight: 700
|
||||
lineHeight: 1.25
|
||||
letterSpacing: "-0.025em"
|
||||
title:
|
||||
fontFamily: "Inter, system-ui, sans-serif"
|
||||
fontSize: "1.25rem"
|
||||
fontWeight: 600
|
||||
lineHeight: 1.4
|
||||
body:
|
||||
fontFamily: "Inter, system-ui, sans-serif"
|
||||
fontSize: "0.875rem"
|
||||
fontWeight: 400
|
||||
lineHeight: 1.6
|
||||
label:
|
||||
fontFamily: "Inter, system-ui, sans-serif"
|
||||
fontSize: "0.75rem"
|
||||
fontWeight: 600
|
||||
letterSpacing: "0.025em"
|
||||
rounded:
|
||||
sm: "6px"
|
||||
md: "8px"
|
||||
lg: "10px"
|
||||
xl: "14px"
|
||||
"2xl": "18px"
|
||||
"3xl": "22px"
|
||||
"4xl": "26px"
|
||||
spacing:
|
||||
card-sm: "12px"
|
||||
card-default: "16px"
|
||||
components:
|
||||
button-primary:
|
||||
backgroundColor: "{colors.marigold-orange}"
|
||||
textColor: "{colors.pure-white}"
|
||||
rounded: "{rounded.lg}"
|
||||
height: "32px"
|
||||
padding: "0 10px"
|
||||
button-primary-hover:
|
||||
backgroundColor: "{colors.marigold-orange-hover}"
|
||||
button-outline:
|
||||
backgroundColor: "{colors.porch-white}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.lg}"
|
||||
height: "32px"
|
||||
badge-default:
|
||||
backgroundColor: "{colors.marigold-orange}"
|
||||
textColor: "{colors.pure-white}"
|
||||
rounded: "{rounded.4xl}"
|
||||
height: "20px"
|
||||
padding: "2px 8px"
|
||||
badge-tint:
|
||||
backgroundColor: "{colors.marigold-tint}"
|
||||
textColor: "{colors.marigold-deep}"
|
||||
rounded: "{rounded.4xl}"
|
||||
height: "24px"
|
||||
padding: "4px 12px"
|
||||
card:
|
||||
backgroundColor: "{colors.pure-white}"
|
||||
rounded: "{rounded.xl}"
|
||||
padding: "16px"
|
||||
input:
|
||||
backgroundColor: "transparent"
|
||||
rounded: "{rounded.lg}"
|
||||
height: "32px"
|
||||
padding: "4px 10px"
|
||||
---
|
||||
|
||||
# Design System: PawFeed
|
||||
|
||||
## Overview
|
||||
|
||||
**Creative North Star: "The Spotlight Porch"**
|
||||
|
||||
PawFeed reads like warm porch light, not a stage floodlight: a single, calm pool of light where one animal at a time gets to be the whole picture, away from the loud street of an algorithmic feed outside. The system is deliberately quiet — soft neutral canvases, gentle rounded edges, and a single confident marigold-orange accent used sparingly enough that it always reads as "this matters" rather than as house color washed over everything. Definition comes from thin rings and warm neutrals, not drop shadows; nothing fights for attention the way an ad or a sponsored post would.
|
||||
|
||||
The tone is warm, calm, and a little playful — the playfulness lives in small, earned moments (the paw-print story ring, the spring-and-burst paw reaction, the founder's own voice on the About page) rather than in loud graphics or forced trends. This is explicitly not the visual language of an algorithmic, ad-heavy feed: no dense card grids competing for eyeballs, no aggressive color blocking, no urgency-driven UI. It is closer to a real photo wall than a curated gallery — personal, unpretentious, and built around the animal in frame.
|
||||
|
||||
**Key Characteristics:**
|
||||
- A single accent color (marigold orange), used deliberately and sparingly — never as a wash.
|
||||
- Flat, ring-defined surfaces; shadows appear only as a hover/interaction response, never at rest.
|
||||
- Soft, consistently rounded geometry at every scale, from 6px controls to fully pill-shaped badges.
|
||||
- One typeface (Inter) doing all the work — hierarchy comes from weight and size, not from mixing families.
|
||||
- Warm and calm by default, with small, specific moments of playfulness (paw motifs, spring animations) rather than constant visual noise.
|
||||
|
||||
## Colors
|
||||
|
||||
A quiet, mostly neutral palette with one warm, confident accent — the accent is the point precisely because it is rare.
|
||||
|
||||
### Primary
|
||||
- **Warm Marigold Orange** (`oklch(0.7024 0.1967 41.45)`, ≈ `#f97316`): the one color that means "act here" or "this is alive" — primary buttons, active/selected states, the paw-reaction fill, focus rings, and the recurring paw-print/iconography motif. Also appears as a light wash, **Marigold Tint** (`#ffedd5`), for kicker pills and soft badges, and as a deeper step, **Marigold Deep** (`#c2410c`), in the rare full-bleed CTA gradient panel.
|
||||
|
||||
### Neutral
|
||||
- **Porch White** (`oklch(0.9882 0 0)`): the app's resting canvas — a hair off pure white, never stark.
|
||||
- **Pure White** (`oklch(1 0 0)`): cards, popovers, and anything meant to sit visibly above the canvas.
|
||||
- **Ink** (`oklch(0.145 0 0)`): primary text.
|
||||
- **Hairline Gray** (`oklch(0.922 0 0)`): borders and input strokes — always thin, never a heavy divider.
|
||||
- **Whisper Gray** (`oklch(0.9529 0 0)`): secondary surfaces and muted backgrounds (pills, subtle fills).
|
||||
- **Quiet Gray** (`oklch(0.556 0 0)`): secondary/muted text — timestamps, captions, helper copy.
|
||||
- **Alert Red** (`oklch(0.577 0.245 27.325)`): the one status color, reserved for destructive actions and validation errors — always shown as a soft tint (`/10`–`/20` opacity), never a solid alarming block.
|
||||
|
||||
### Named Rules
|
||||
**The One Accent Rule.** Marigold Orange is the only color allowed to mean "important" or "active." It is never used decoratively or as a large fill — if orange is on screen, it is doing a job (CTA, active state, reaction, focus).
|
||||
|
||||
**The Dark-Mode Drift Fact.** This is an observed inconsistency, not a design decision to imitate: in dark mode, the `--primary` and `--ring` CSS variables currently fall back to neutral grayscale (`oklch(0.922 0 0)` / `oklch(0.556 0 0)`) instead of carrying the marigold hue forward. The product's actual orange identity survives in dark mode only because most CTAs and accents are hardcoded with literal `orange-500`/`600` Tailwind utility classes rather than the semantic `bg-primary`/`text-primary` tokens. Treat the semantic primary/ring tokens as unreliable for brand color in dark mode until this is reconciled; use the literal Marigold Orange value directly when a dark-mode element must stay on-brand.
|
||||
|
||||
## Typography
|
||||
|
||||
**Display/Body Font:** Inter (Google Font, `latin` subset, loaded via `next/font`), with a `system-ui` fallback stack. One family for the entire product — there is no separate display or mono face in active use.
|
||||
|
||||
**Character:** Confident but unfussy — a standard humanist grotesque doing hierarchy through weight and scale rather than personality. It reads as approachable and modern, never corporate-cold or ornamental.
|
||||
|
||||
### Hierarchy
|
||||
- **Display** (800, `text-3xl` → `sm:text-4xl` → `lg:text-5xl`, tight tracking): hero headlines on marketing pages (Landing, Über uns).
|
||||
- **Headline** (700, `text-2xl` → `sm:text-3xl`, tight tracking): section headings within marketing pages, CTA banners.
|
||||
- **Title** (600, `text-xl`): in-app page titles (e.g. "Meine Tiere", pet Insights, Health) and card titles.
|
||||
- **Body** (400, `text-sm` for UI copy; `text-base` → `sm:text-lg` with relaxed line-height for marketing prose): the default reading size throughout the app; marketing paragraphs run slightly larger and looser.
|
||||
- **Label** (600, `text-xs`, uppercase, `tracking-wide`): kicker pills, section eyebrows, stat captions — always paired with a soft-tint background (see Marigold Tint).
|
||||
|
||||
### Named Rules
|
||||
**The One-Family Rule.** Every weight and size comes from Inter. Hierarchy is built with scale and weight, never by introducing a second typeface.
|
||||
|
||||
## Layout
|
||||
|
||||
Content is column-constrained and centered, not full-bleed — the platform never asks the eye to travel edge-to-edge. In-app utility pages (pet list, profile, health, insights, messages, notifications) sit in a `max-w-[640px]` centered column; marketing sections widen slightly (`max-w-2xl`–`max-w-4xl`) for breathing room around prose and stat grids; the post detail view is the one deliberately wide surface (`max-w-[960px]`), splitting into an image column plus a fixed side panel on desktop and stacking on mobile.
|
||||
|
||||
Responsive padding follows a consistent `px-4` → `sm:px-6` step, and grids resize by adding columns rather than by changing density: pet cards go `grid-cols-2` → `md:grid-cols-3`, stat tiles go single-column → `sm:grid-cols-3`. Mobile-first throughout; nothing depends on a hover-only affordance to be usable.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
Flat by default. Surfaces are separated with a 1px ring (`ring-1 ring-foreground/10` on cards) rather than a resting drop shadow — depth is tonal and linear, not lit. A shadow only appears as a direct response to interaction (e.g. a stat card lifting slightly with `hover:shadow-md` and `hover:-translate-y-1` on the About page), never at rest.
|
||||
|
||||
### Named Rules
|
||||
**The Ring-Not-Shadow Rule.** A resting surface gets a thin ring for definition, not a box-shadow. Shadows are earned by interaction (hover, active elevation), never applied by default.
|
||||
|
||||
## Shapes
|
||||
|
||||
Soft and consistently rounded at every scale, driven by a single `--radius` base (10px) that every other step multiplies: 6px (small controls) → 8px → 10px (buttons, inputs, the system default) → 14px (cards) → 18–22px (larger containers/dialogs) → 26px (badges, pills — effectively full round on their height). Borders are hairline and low-contrast (`hairline-gray`) rather than heavy outlines; nothing in the system uses a sharp, unrounded corner.
|
||||
|
||||
## Components
|
||||
|
||||
### Buttons
|
||||
- **Shape:** `rounded-lg` (10px), 32px default height.
|
||||
- **Primary:** Marigold Orange fill, white text; many hero/CTA buttons instead hardcode the orange literal directly rather than routing through the `default` variant — see the Dark-Mode Drift Fact above for why that matters.
|
||||
- **Hover / Focus:** hover darkens toward Marigold Orange Hover (`#ea580c`-equivalent, via `/80` opacity on the token); focus shows a soft 3px ring in the accent color plus a matching border. A pressed button nudges down by 1px — a small, tactile confirmation rather than a color flash.
|
||||
- **Secondary / Outline / Ghost:** outline uses a hairline border on the canvas color; ghost has no resting fill at all, appearing only on hover; destructive actions use a soft red tint (`/10`–`/20`), never a solid red block.
|
||||
|
||||
### Chips / Badges
|
||||
- **Style:** fully pill-shaped (`rounded-4xl`, 26px — effectively a stadium shape at badge height). Default variant is a solid Marigold Orange fill with white text; the more common "kicker" variant is a soft **Marigold Tint** background with **Marigold Deep** text, paired with uppercase Label type.
|
||||
- **State:** no distinct selected/unselected treatment observed outside dropdown/filter contexts; badges are primarily informational, not interactive controls.
|
||||
|
||||
### Cards / Containers
|
||||
- **Corner Style:** `rounded-xl` (14px).
|
||||
- **Background:** Pure White on the Porch White canvas.
|
||||
- **Shadow Strategy:** none at rest — see Elevation & Depth. Definition comes from `ring-1 ring-foreground/10`.
|
||||
- **Border:** the ring itself; no separate border token.
|
||||
- **Internal Padding:** 16px default, 12px in the compact (`size="sm"`) variant — both driven by a `--card-spacing` custom property, not a hardcoded utility.
|
||||
|
||||
### Inputs / Fields
|
||||
- **Style:** `rounded-lg` (10px), transparent background, hairline border, 32px height.
|
||||
- **Focus:** border shifts to the accent ring color plus a soft 3px glow ring — the same focus language as buttons.
|
||||
- **Error / Disabled:** invalid fields get a red border and red-tinted ring; disabled fields fade to 50% opacity with a faint tinted fill.
|
||||
|
||||
### Paw Reaction (signature)
|
||||
The product's core "like" action is a paw-print icon that snaps into a filled, saturated Marigold Orange with a quick spring (scale 1 → 1.3 → 0.92 → 1, ~0.4s) plus a second ghost paw that bursts outward and fades (scale 0.6 → 2.1, opacity 0.9 → 0, ~0.5s) — a small, specific piece of delight rather than a generic heart-burst animation, and it respects `prefers-reduced-motion` by disabling both.
|
||||
|
||||
### Paw Ring (signature)
|
||||
Story indicators are not a generic solid-color ring around the avatar; they are drawn as an actual paw-print outline (a main pad circle plus four toe-bean circles) in SVG — Marigold Orange for an unseen story, neutral gray once all of a pet's stories have been seen, and no ring at all when there is nothing to see. This is the single most on-brand piece of UI in the product: the "paw" motif is literal, not just a name.
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### Do:
|
||||
- **Do** treat Marigold Orange as rare and meaningful — one clear "this is the action" per view, not a wash of brand color.
|
||||
- **Do** define surface separation with a hairline ring, and reserve shadows for hover/interaction states only.
|
||||
- **Do** keep every corner rounded, scaling the radius with the element's size rather than mixing sharp and soft shapes.
|
||||
- **Do** carry the paw motif (icon, ring, reaction animation) as literal iconography, not just a name — it is the product's signature.
|
||||
- **Do** respect `prefers-reduced-motion` on every custom animation, matching the existing paw-reaction and scroll-reveal treatments.
|
||||
|
||||
### Don't:
|
||||
- **Don't** introduce a second typeface — hierarchy comes from Inter's weight/size scale alone.
|
||||
- **Don't** trust `bg-primary`/`--ring` for brand-orange in dark mode without checking — they currently resolve to neutral gray (see the Dark-Mode Drift Fact); use the literal Marigold Orange value when the dark-mode result must stay on-brand.
|
||||
- **Don't** add resting drop shadows to cards or panels — depth is tonal (rings, warm neutrals), not lit.
|
||||
- **Don't** fill large areas with solid orange (banners, hero backgrounds) outside the one existing gradient CTA panel — the accent's power comes from scarcity.
|
||||
- **Don't** use sharp, unrounded corners anywhere in the system.
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
# Product
|
||||
|
||||
<!-- impeccable:product-schema 1 -->
|
||||
|
||||
## Platform
|
||||
|
||||
web
|
||||
|
||||
## Users
|
||||
|
||||
Primary audience: dog and cat owners — the core target for messaging, product priority, and design decisions. Every other supported species (birds, horses, rabbits, guinea pigs, hamsters, mice, rats, chinchillas, snakes, lizards, turtles, geckos, spiders, aquariums) is fully first-class in the data model and feature set (own Species/Breed taxonomy, same posting/story/health capabilities) but secondary — a niche audience, not the one design and copy should be optimized around by default.
|
||||
|
||||
Situation: pet owners tired of their animal being a supporting prop on a human-first, ad-heavy, trend-driven social feed. They want a calm space that exists only for their pet's life — ordinary daily moments, adoption stories, health milestones — without scrolling past ads or forced lifestyle content to get there.
|
||||
|
||||
## Product Purpose
|
||||
|
||||
PawFeed is a social platform where the pet — not the owner — is the identity: its own profile, posts, followers, DMs, 24h stories, milestones, and health record. The owner manages the account but is never the "user" the platform is about. Exists to give pet owners a space that is only about the animals people love. Success = the pet visibly reads as the star (its own profile, its own social graph), not content buried in a human's feed.
|
||||
|
||||
## Positioning
|
||||
|
||||
The mechanism a neighboring product (an Instagram/TikTok pet account) cannot truthfully copy: the entire social graph — posts, follows, reactions, comments, DMs — is pet-scoped, not owner-scoped, at the data-model level, not just in UI framing. Combined with an explicit no-ads, no-forced-trends, no-lifestyle-noise promise (confirmed founder positioning, see Brand Commitments). A pet account on a human-first platform is still hosted inside that platform's human-centric algorithm and ad model; PawFeed's graph has no owner columns at all.
|
||||
|
||||
## Operating Context
|
||||
|
||||
- Web-first, mobile-responsive — no native apps.
|
||||
- Short-form video only (Mux-powered), no long-form.
|
||||
- Pre-launch invite gate currently active (`INVITE_REQUIRED`); **official public launch is 2026-09-01** (stated on the live "Über uns" page as a real, binding date, not a placeholder).
|
||||
- Species/breed taxonomy is fully data-driven (`prisma/seed.ts`) — dogs and cats carry the complete official FCI/FIFe breed lists; every other species has its own curated breed list.
|
||||
- Self-hosted deployment: OpenMediaVault NAS, Docker, 3 load-balanced replicas behind Nginx Proxy Manager — not Vercel, despite `next.config.ts` output settings suggesting otherwise.
|
||||
- Legal/privacy-conscious: GDPR data export, health data private by default (shareable only via a tokenized Health Card link), AI-content disclosure requirement on posts/stories, an owner-level "Arachnophobie-Helfer" opt-out that fully excludes spider content from feed/explore/search.
|
||||
- Content and UI are currently German-first (all live copy); `next-intl` scaffolding (`en`/`de` catalogs) exists with further locales planned post-MVP.
|
||||
|
||||
## Capabilities and Constraints
|
||||
|
||||
- Two-layer identity: Clerk manages owner accounts for auth only; `Pet` is the app-domain social identity.
|
||||
- Photos, short-form video, 24h stories (with paw reactions and comments), milestones, pet-to-pet DMs.
|
||||
- Health tracking: weight logs, vet visits, vaccines, a shareable Health Card.
|
||||
- Admin/moderation panel gated by a secret path segment plus role (`SUPER_ADMIN`/`MODERATOR`).
|
||||
- Owner-facing insights: a small analytics dashboard per pet (reactions/comments received, top post/story) reachable from the pet's own profile.
|
||||
- Undecided/open: further locale rollout (ES/IT/FR) has no committed timeline.
|
||||
|
||||
## Brand Commitments
|
||||
|
||||
- **Name "PawFeed"** — fixed.
|
||||
- **Personal, warm, solo-founder voice** — the "Über uns" origin story (a late-2025 night scrolling ad-heavy lifestyle feeds, wanting a space only for animals) is a real, binding founder narrative, not disposable marketing copy; future copy should preserve that first-person, heartfelt register rather than flattening it into generic corporate voice.
|
||||
- **Orange as the core brand color** (Tailwind `orange-500`/`600` family) — used consistently for CTAs, active/selected states, badges, and accent elements across the whole product.
|
||||
- **"Paw" motif and terminology, recurring** — paw-print iconography, "Paw" as the reaction verb/name, `PawRing` (story ring), `PawBack` — this vocabulary is product identity, not incidental naming.
|
||||
- **Launch date 2026-09-01** — real and referenced live on the marketing site; do not treat as a stale/placeholder date.
|
||||
|
||||
## Evidence on Hand
|
||||
|
||||
- Live, real-time public counters (member/pet/sponsor counts) on `/ueber-uns`, sourced from the database (`stats.getPublicStats`) — explicitly framed on-page as "keine Marketing-Zahlen" (not marketing numbers). Safe to reference as real evidence; never replace with invented figures.
|
||||
- No testimonials, case studies, or press exist yet — pre-launch. Do not fabricate any.
|
||||
- A soft/beta period has been running since 2026-08-10; the *official* public launch is 2026-09-01.
|
||||
|
||||
## Product Principles
|
||||
|
||||
- The pet is the profile, the owner never is — every feature should route through `petId`, not `ownerId`.
|
||||
- No ads, no forced trends, no algorithmic outrage — the calm, animal-only space is the product, not a growth-phase compromise.
|
||||
- Dogs and cats are the design/messaging priority; every other supported species stays fully functional and equally dignified in the product, just not the lead audience.
|
||||
- Health and everyday-life documentation matter as much as social posting — this is a utility for the pet's life, not just a content feed.
|
||||
- Privacy-conscious by default: health data private unless explicitly shared, GDPR export available, opt-outs (e.g. spider content) respected everywhere content is surfaced.
|
||||
|
||||
## Accessibility & Inclusion
|
||||
|
||||
No product-specific accessibility requirement has been established beyond the general WCAG 2.2 AA practice already followed in this codebase's engineering standards.
|
||||
+1
-1
@@ -686,7 +686,7 @@
|
||||
"errorToast": "Tier konnte nicht blockiert werden. Bitte versuch es erneut."
|
||||
},
|
||||
"Landing": {
|
||||
"tagline": "Wo Haustiere die Stars sind. Profile, Beiträge und Storys — gemacht für Hunde, Katzen und Vögel.",
|
||||
"tagline": "Wo Haustiere die <stars>Stars</stars> sind. Profile, Beiträge und Storys — gemacht für Hunde, Katzen und Vögel.",
|
||||
"getStarted": "Jetzt starten",
|
||||
"logIn": "Anmelden",
|
||||
"previewAlt": "Der PawFeed-Feed mit Tierbeiträgen von Noctis und Bubi",
|
||||
|
||||
+1
-1
@@ -686,7 +686,7 @@
|
||||
"errorToast": "Couldn't block this pet. Please try again."
|
||||
},
|
||||
"Landing": {
|
||||
"tagline": "Where pets are the stars. Profiles, posts, and stories — built for dogs, cats, and birds.",
|
||||
"tagline": "Where pets are the <stars>stars</stars>. Profiles, posts, and stories — built for dogs, cats, and birds.",
|
||||
"getStarted": "Get started",
|
||||
"logIn": "Log in",
|
||||
"previewAlt": "The PawFeed feed, showing pet posts from Noctis and Bubi",
|
||||
|
||||
@@ -195,7 +195,7 @@ export default function DatenschutzPage() {
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<div className="bg-card border rounded-xl p-6 flex items-center justify-between">
|
||||
<div className="pt-6 flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">Weitere rechtliche Informationen</p>
|
||||
<div className="flex gap-4">
|
||||
<Link href="/impressum" className="text-sm text-orange-600 dark:text-orange-400 hover:underline">
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function ImpressumPage() {
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<div className="bg-card border rounded-xl p-6 flex items-center justify-between">
|
||||
<div className="pt-6 flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">Weitere rechtliche Informationen</p>
|
||||
<div className="flex gap-4">
|
||||
<Link href="/datenschutz" className="text-sm text-orange-600 dark:text-orange-400 hover:underline">
|
||||
|
||||
@@ -60,6 +60,36 @@ export default function NutzungsbedingungenPage() {
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection number="3a" title="Sponsoring">
|
||||
<p>
|
||||
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{" "}
|
||||
<Link href="/datenschutz" className="text-orange-600 dark:text-orange-400 hover:underline">
|
||||
Datenschutzerklärung
|
||||
</Link>
|
||||
).
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
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.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
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.
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection number="4" title="Verbotene Inhalte & Verhaltensregeln">
|
||||
<p className="mb-2">Folgende Inhalte und Verhaltensweisen sind auf PawFeed untersagt:</p>
|
||||
<ul className="space-y-1.5">
|
||||
@@ -186,37 +216,7 @@ export default function NutzungsbedingungenPage() {
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<LegalSection number="12" title="Sponsoring">
|
||||
<p>
|
||||
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{" "}
|
||||
<Link href="/datenschutz" className="text-orange-600 dark:text-orange-400 hover:underline">
|
||||
Datenschutzerklärung
|
||||
</Link>
|
||||
).
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
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.
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
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.
|
||||
</p>
|
||||
</LegalSection>
|
||||
|
||||
<div className="bg-card border rounded-xl p-6 flex items-center justify-between">
|
||||
<div className="pt-6 flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">Weitere rechtliche Informationen</p>
|
||||
<div className="flex gap-4">
|
||||
<Link href="/impressum" className="text-sm text-orange-600 dark:text-orange-400 hover:underline">
|
||||
|
||||
+115
-86
@@ -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() {
|
||||
<LandingHeader />
|
||||
|
||||
<main>
|
||||
{/* Hero */}
|
||||
<section className="px-4 pt-8 sm:px-6 sm:pt-10">
|
||||
<div className="relative mx-auto max-w-6xl overflow-hidden rounded-4xl bg-gradient-to-br from-orange-400 via-orange-500 to-orange-700">
|
||||
<PawPrint
|
||||
className="hero-parallax-icon pointer-events-none absolute -right-24 -top-20 h-[480px] w-[480px] text-white opacity-[0.14] sm:-right-16"
|
||||
fill="white"
|
||||
strokeWidth={0}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className="relative z-10 grid gap-12 px-6 py-16 sm:px-10 sm:py-24 lg:grid-cols-2 lg:items-center lg:gap-8">
|
||||
{/* 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. */}
|
||||
<section className="relative overflow-hidden px-4 pt-14 pb-20 sm:px-6 sm:pt-20 sm:pb-28 lg:px-10">
|
||||
<div className="mx-auto grid max-w-6xl gap-14 lg:grid-cols-2 lg:items-center lg:gap-10">
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-5xl font-extrabold tracking-tight text-white sm:text-6xl lg:text-7xl">
|
||||
PawFeed
|
||||
<h1 className="text-4xl font-extrabold tracking-tight text-foreground sm:text-5xl lg:text-6xl">
|
||||
{t.rich("tagline", {
|
||||
stars: (chunks) => (
|
||||
<span className="text-orange-600 dark:text-orange-400">{chunks}</span>
|
||||
),
|
||||
})}
|
||||
</h1>
|
||||
<p className="mx-auto mt-5 max-w-lg text-lg font-medium text-orange-50 sm:text-xl lg:mx-0 lg:text-2xl">
|
||||
{t("tagline")}
|
||||
</p>
|
||||
|
||||
<div className="mt-10 flex flex-col justify-center gap-3 sm:flex-row lg:justify-start">
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="inline-flex items-center justify-center rounded-xl bg-white px-7 py-3.5 text-base font-semibold text-orange-700 shadow-lg transition-transform hover:-translate-y-0.5 hover:bg-orange-50"
|
||||
className="inline-flex items-center justify-center rounded-xl bg-orange-500 px-7 py-3.5 text-base font-semibold text-white transition-colors hover:bg-orange-600"
|
||||
>
|
||||
{t("getStarted")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/log-in"
|
||||
className="inline-flex items-center justify-center rounded-xl border-2 border-white/70 px-7 py-3.5 text-base font-semibold text-white transition-colors hover:bg-white/10"
|
||||
className="inline-flex items-center justify-center rounded-xl border border-border px-7 py-3.5 text-base font-semibold text-foreground transition-colors hover:bg-muted"
|
||||
>
|
||||
{t("logIn")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phone mockup + floating badges */}
|
||||
<div className="relative mx-auto w-full max-w-[320px] lg:mx-0 lg:justify-self-end">
|
||||
<div className="relative rounded-[3rem] bg-neutral-900 p-2.5 shadow-2xl ring-1 ring-white/10">
|
||||
{/* Phone mockup — sits inside one soft marigold glow, the hero's
|
||||
single spotlight moment, not a hero-wide wash. */}
|
||||
<div className="porch-glow relative mx-auto w-full max-w-[320px] lg:mx-0 lg:justify-self-end">
|
||||
<div
|
||||
className="pointer-events-none absolute left-1/2 top-1/2 -z-10 h-[420px] w-[420px] -translate-x-1/2 -translate-y-1/2 rounded-full opacity-70 blur-3xl"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, oklch(0.7024 0.1967 41.45 / 35%) 0%, oklch(0.7024 0.1967 41.45 / 0%) 70%)",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div className="relative rounded-[3rem] bg-neutral-900 p-2.5 ring-1 ring-foreground/10">
|
||||
<span className="absolute -left-px top-24 h-8 w-[3px] rounded-l-sm bg-neutral-950" aria-hidden="true" />
|
||||
<span className="absolute -left-px top-36 h-12 w-[3px] rounded-l-sm bg-neutral-950" aria-hidden="true" />
|
||||
<span className="absolute -right-px top-32 h-16 w-[3px] rounded-r-sm bg-neutral-950" aria-hidden="true" />
|
||||
@@ -105,83 +113,104 @@ export default async function RootPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Two grounded annotations, not three — ring-defined like the
|
||||
rest of the system, not shadow-heavy floating chips. */}
|
||||
<div
|
||||
className="float-badge absolute -left-10 top-24 hidden items-center gap-2 rounded-2xl bg-white px-3 py-2 shadow-xl sm:flex"
|
||||
className="float-badge absolute -left-8 top-20 hidden items-center gap-2 rounded-2xl bg-card px-3 py-2 ring-1 ring-foreground/10 sm:flex"
|
||||
style={{ animationDelay: "0.5s" }}
|
||||
>
|
||||
<Heart className="h-4 w-4 shrink-0 fill-orange-500 text-orange-500" aria-hidden="true" />
|
||||
<span className="text-xs font-semibold text-zinc-900">{t("heroBadge2")}</span>
|
||||
<span className="text-xs font-semibold text-foreground">{t("heroBadge1")}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="float-badge absolute -right-6 top-1/2 hidden items-center gap-2 rounded-2xl bg-white px-3 py-2 shadow-xl sm:flex"
|
||||
style={{ animationDelay: "1.2s" }}
|
||||
className="float-badge absolute -right-6 bottom-10 hidden items-center gap-2 rounded-2xl bg-card px-3 py-2 ring-1 ring-foreground/10 sm:flex"
|
||||
style={{ animationDelay: "1.6s" }}
|
||||
>
|
||||
<Sparkles className="h-4 w-4 shrink-0 text-orange-500" aria-hidden="true" />
|
||||
<span className="text-xs font-semibold text-zinc-900">{t("heroBadge3")}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="float-badge absolute -bottom-8 left-1/4 hidden items-center gap-2 rounded-2xl bg-white px-3 py-2 shadow-xl sm:flex"
|
||||
style={{ animationDelay: "2.4s" }}
|
||||
>
|
||||
<PawPrint className="h-4 w-4 shrink-0 fill-orange-500 text-orange-500" aria-hidden="true" />
|
||||
<span className="text-xs font-semibold text-zinc-900">{t("heroBadge1")}</span>
|
||||
<span className="text-xs font-semibold text-foreground">{t("heroBadge3")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{/* Catchphrase + feature grid */}
|
||||
<section className="px-6 py-24 sm:px-10">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<ScrollReveal className="text-center" delayMs={100}>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||
{t("catchHeading")}
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-base text-muted-foreground sm:text-lg">
|
||||
{t("catchSubline")}
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
{/* Features — one dominant mechanism, three supporting facts.
|
||||
Not four identical icon/heading/text cards. */}
|
||||
<section className="px-6 py-24 sm:px-10">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<ScrollReveal className="text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||
{t("catchHeading")}
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-base text-muted-foreground sm:text-lg">
|
||||
{t("catchSubline")}
|
||||
</p>
|
||||
</ScrollReveal>
|
||||
|
||||
<div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{features.map(({ icon: Icon, title, body }, index) => (
|
||||
<ScrollReveal key={title} delayMs={200 + index * 100}>
|
||||
<div className="group h-full rounded-2xl border border-border/60 bg-card p-6 transition-all duration-300 hover:-translate-y-1 hover:border-orange-500/30 hover:shadow-md">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-orange-100 text-orange-600 transition-transform duration-300 group-hover:scale-105">
|
||||
<Icon className="h-5.5 w-5.5" aria-hidden="true" />
|
||||
</div>
|
||||
<h3 className="mt-4 text-base font-semibold text-foreground">{title}</h3>
|
||||
<p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">{body}</p>
|
||||
<div className="mt-16 grid gap-12 lg:grid-cols-5 lg:gap-16">
|
||||
{/* Dominant feature — the actual mechanism, given real weight */}
|
||||
<ScrollReveal className="relative lg:col-span-3">
|
||||
{/* Same PawPrint mark as the header/footer logo — a
|
||||
different hand-drawn paw here (as before) read as an
|
||||
off-brand, unrelated shape. */}
|
||||
<PawPrint
|
||||
className="pointer-events-none absolute -left-4 -top-6 h-40 w-40 text-orange-500/10 lg:-left-8 lg:h-56 lg:w-56"
|
||||
fill="currentColor"
|
||||
strokeWidth={0}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="relative">
|
||||
<h3 className="text-2xl font-bold tracking-tight text-foreground sm:text-3xl">
|
||||
{t("feature1Title")}
|
||||
</h3>
|
||||
<p className="mt-3 max-w-md text-base leading-relaxed text-muted-foreground sm:text-lg">
|
||||
{t("feature1Body")}
|
||||
</p>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Detail feature card */}
|
||||
<section className="px-4 pb-24 sm:px-6">
|
||||
<ScrollReveal className="mx-auto max-w-6xl">
|
||||
<div className="grid gap-10 rounded-4xl bg-muted/40 px-6 py-14 sm:px-10 sm:py-16 lg:grid-cols-2 lg:items-center lg:gap-16">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||
{t("detailHeading")}
|
||||
</h2>
|
||||
<p className="mt-4 text-base text-muted-foreground sm:text-lg">{t("detailBody")}</p>
|
||||
{/* Supporting facts — a quiet list, not boxed cards */}
|
||||
<ScrollReveal className="divide-y divide-border lg:col-span-2" delayMs={100}>
|
||||
{supportingFeatures.map(({ icon: Icon, title, body }) => (
|
||||
<div key={title} className="flex gap-4 py-5 first:pt-0 last:pb-0">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-orange-100 text-orange-600 dark:bg-orange-500/15 dark:text-orange-400">
|
||||
<Icon className="h-4.5 w-4.5" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-foreground">{title}</h4>
|
||||
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">{body}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
<ul className="space-y-4">
|
||||
{[t("detailPoint1"), t("detailPoint2")].map((point) => (
|
||||
<li key={point} className="flex items-start gap-3 rounded-2xl bg-background p-4 shadow-sm">
|
||||
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-orange-500" aria-hidden="true" />
|
||||
<span className="text-sm text-foreground sm:text-base">{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{/* Detail panel — flat, ring-defined, no drop shadows */}
|
||||
<section className="px-4 pb-24 sm:px-6">
|
||||
<ScrollReveal className="mx-auto max-w-6xl">
|
||||
<div className="grid gap-10 rounded-4xl bg-muted/40 px-6 py-14 sm:px-10 sm:py-16 lg:grid-cols-2 lg:items-center lg:gap-16">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||
{t("detailHeading")}
|
||||
</h2>
|
||||
<p className="mt-4 text-base text-muted-foreground sm:text-lg">{t("detailBody")}</p>
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
{[t("detailPoint1"), t("detailPoint2")].map((point) => (
|
||||
<li
|
||||
key={point}
|
||||
className="flex items-start gap-3 rounded-2xl bg-background p-4 ring-1 ring-foreground/10"
|
||||
>
|
||||
<CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-orange-500" aria-hidden="true" />
|
||||
<span className="text-sm text-foreground sm:text-base">{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</ScrollReveal>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
{/* Footer — repeats the navbar */}
|
||||
|
||||
@@ -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 (
|
||||
<header className="sticky top-0 z-20 border-b border-border/60 bg-background/80 px-4 py-4 backdrop-blur-md sm:px-6 lg:px-10">
|
||||
<header className="landing-header-hairline sticky top-0 z-20 bg-background px-4 py-4 sm:px-6 lg:px-10">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<PawPrint className="h-6 w-6 text-orange-500" fill="currentColor" strokeWidth={0} aria-hidden="true" />
|
||||
<Link href="/" className="landing-logo flex items-center gap-2">
|
||||
<PawPrint
|
||||
className="landing-logo-paw h-6 w-6 text-orange-500"
|
||||
fill="currentColor"
|
||||
strokeWidth={0}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-lg font-bold text-foreground">PawFeed</span>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden items-center gap-6 text-sm font-medium text-muted-foreground lg:flex">
|
||||
{navLinks.map((link) => (
|
||||
<Link key={link.label} href={link.href} className="transition-colors hover:text-foreground">
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<LandingNavLinks links={navLinks} />
|
||||
|
||||
<div className="flex items-center gap-1 sm:gap-3">
|
||||
<ThemeToggleButton />
|
||||
@@ -45,7 +45,7 @@ export async function LandingHeader() {
|
||||
</Link>
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="inline-flex items-center justify-center whitespace-nowrap rounded-full bg-orange-700 px-3 py-2 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-orange-800 sm:px-4"
|
||||
className="inline-flex items-center justify-center whitespace-nowrap rounded-lg bg-orange-500 px-3 py-2 text-sm font-semibold text-white transition-colors hover:bg-orange-600 sm:px-4"
|
||||
>
|
||||
{t("navSignUp")}
|
||||
</Link>
|
||||
|
||||
@@ -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 (
|
||||
<nav className="hidden items-center gap-6 text-sm font-medium text-muted-foreground lg:flex">
|
||||
{links.map((link) => {
|
||||
const isActive = pathname === link.href;
|
||||
return (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`relative pb-1 transition-colors hover:text-foreground ${
|
||||
isActive ? "text-foreground" : ""
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
{isActive && (
|
||||
<span
|
||||
className="absolute -bottom-1 left-1/2 h-1 w-1 -translate-x-1/2 rounded-full bg-orange-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-muted/30">
|
||||
<div className="max-w-2xl mx-auto px-4 py-10">
|
||||
{/* Back link */}
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="landing-header-hairline max-w-2xl mx-auto px-4 pb-6">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors mb-8 group"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors mt-10 mb-8 group"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 group-hover:-translate-x-0.5 transition-transform" />
|
||||
Zurück zu PawFeed
|
||||
</Link>
|
||||
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
<div className="w-10 h-10 bg-orange-500 rounded-xl flex items-center justify-center shadow-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-orange-500 rounded-xl flex items-center justify-center">
|
||||
<PawPrint className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<span className="text-2xl font-bold tracking-tight">PawFeed</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Page title card */}
|
||||
<div className="bg-card border rounded-2xl px-8 py-7 shadow-sm mb-5">
|
||||
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
|
||||
{subtitle && (
|
||||
<p className="text-sm text-muted-foreground mt-1">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-2xl mx-auto px-4 py-10">
|
||||
{/* Page title */}
|
||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">{title}</h1>
|
||||
{subtitle && <p className="text-sm text-muted-foreground mt-2">{subtitle}</p>}
|
||||
|
||||
{/* Sections */}
|
||||
<div className="space-y-3">{children}</div>
|
||||
<div className="mt-8 divide-y divide-border">{children}</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="mt-10 text-center text-xs text-muted-foreground">
|
||||
@@ -60,16 +65,11 @@ export function LegalSection({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-card border rounded-xl p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className="flex-shrink-0 w-6 h-6 bg-orange-500/10 text-orange-600 dark:text-orange-400 rounded-md text-xs font-bold flex items-center justify-center">
|
||||
{number}
|
||||
</span>
|
||||
<h2 className="text-sm font-semibold text-foreground">{title}</h2>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground leading-relaxed space-y-2 pl-9">
|
||||
<section id={`abschnitt-${number}`} className="py-6 first:pt-0 last:pb-0">
|
||||
<h2 className="text-lg font-semibold tracking-tight text-foreground">{title}</h2>
|
||||
<div className="mt-3 text-sm text-muted-foreground leading-relaxed space-y-2">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user