- explore.getTrending: top-12 photo posts by reaction count (48h window)
and top-10 hashtags by post count (7d window)
- Explore page opens on Trending tab by default; species/breed tabs
preserved with breed filter hidden when not applicable
- Trending UI: hashtag chips linking to /hashtag/[tag] + 3-col post
grid with paw-count overlay and PostCard detail dialog
feat(follows): auto-trigger follower milestone posts
- After each follow, count followers for followeePetId and check against
all thresholds (500 / 1K / 2.5K / 5K / 10K / 25K / 50K / 100K /
250K / 500K / 1M)
- Guard against duplicate awards via existing Milestone lookup
- Fire-and-forget: milestone post created + fanned out to follower feeds
without blocking the follow response
Emergency vet contact data (phone, address, map) was visible on the
token-shared health card URL. Health data is owner-private; the health
card is the single sharing exception but should not include internal
emergency contact details.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EmergencyVet model: name, address, phone, website, lat, lng (1:1 to Pet)
- health router: getEmergencyVet, setEmergencyVet, deleteEmergencyVet
- EmergencyVetSection: Nominatim search (debounced 500ms, OSM attribution),
result dropdown auto-fills address + coordinates, manual override for all fields
- OSM iframe map (no API key) with "In Karte öffnen" link; shown in edit
mode as coordinate preview and in view mode after save
- Health card (public): red-accented emergency vet block with phone link,
website link and OSM map; map hidden on print (print:hidden)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sheet side=bottom was anchored to screen bottom edge
- Default SheetContent X button overlapped PostCard 3-dot menu
- Dialog renders centered, showCloseButton=false removes the X
(backdrop click / Escape still closes; 3-dots remain the only top-right control)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ShareDialog: expiry selector (7/14/30/60/90 days, default 14), shows
expiry date after generation, "Als PDF öffnen / drucken" button
- WeightChart extracted to standalone component (WeightChart.tsx) so it
can be shared between HealthDashboard and the public health card page
- Health card page: renders WeightChart when 2+ weight entries exist
- AutoPrint client component: auto-triggers window.print() when card is
opened with ?print=1 (600ms delay for render to settle)
- README: Phase 6 + Phase 7 marked complete with full feature summaries;
project structure and router list updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renders a smooth bezier line chart above the weight log list when
2+ entries exist. No external library — pure SVG with:
- Orange-500 line + gradient area fill (PawFeed accent)
- Hover tooltip with dark bg, orange weight value, muted date
- Glow ring + dot state change on hover
- Bezier curve interpolation for smooth line
- Responsive via viewBox (maxHeight 190px)
- Y-axis: auto-scaled with 4 grid ticks
- X-axis: up to 5 date labels (DD.MM), always shows first + last
- Trend delta label (neutral, context-dependent for pets)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Schema: WeightLog, VetVisit, Vaccine, HealthCard models
Router: health.ts — CRUD for weight/vet/vaccines + health card token management
Pages:
- /pets/[petId]/health — owner-only dashboard (weight, vet visits, vaccines)
- /health-card/[token] — public shareable health card (no auth required)
Profile: Health button added for own pets; Edit + Health side by side
Health data is owner-private; the Health Card link is the only public
access point, shareable via token URL (vet, friends, sitters).
Token can be regenerated to revoke access.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add pets.getProfile procedure (no ownership check) so any auth'd user can view any pet profile
- Profile page now uses getProfile; Edit link and owner attribution only shown for own pets
- PostCard header: wrap PawRing + pet name in Link to /pets/[petId]
- Search Pets tab + People tab pet rows: wrap avatar/name in Link to /pets/[petId]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PostCard: add variant="detail" (object-contain + black bg) for profile sheet — full image in original format
- VideoCard: getByPostId polls Mux API directly when PROCESSING, self-heals without webhooks in dev
- PawRing: gray ring for all-seen stories, orange for unseen; reads viewerPetId from ActivePetContext
- Search: add People tab via new search.byOwner (Clerk username search → owner's pets)
- Notifications: rows now navigate to actor profile (FOLLOW) or pet profile (REACTION/COMMENT)
- Notifications router: include post.petId for navigation target
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Schema:
- Add NotificationType enum (FOLLOW, REACTION, COMMENT, MESSAGE)
- Add Notification model with recipientPet, actorPet, post, comment
relations; index on [recipientPetId, read, createdAt desc]
Backend:
- notifications router: list (cursor-paginated), unreadCount,
markRead, markAllRead
- follows.create: fire-and-forget FOLLOW notification to followee
- reactions.toggle: REACTION notification on create (skip self)
- comments.create: COMMENT notification with commentId (skip self)
All triggers use .catch(()=>{}) to never fail the main action
UI:
- NotificationBell component with orange badge, polls every 30s
- /notifications page: list with avatar, type icon, text, thumbnail,
read/unread dot; auto-marks all read on visit
- Sidebar: Notifications link activated with NotificationBell
- MobileNav: Bell replaces Search tab (Search still at /search)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PawRing containers are size+RING_PAD*2=72px tall while the Add Story
button was only 56px, causing vertical misalignment. Wrap the button
in a matching 72px container so all tray items share the same height.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mainCy=58 shifted the ring below the flex-centered avatar, causing the
image to appear above/half-inside the ring. Setting cy=50 aligns the
ring with the container center; toe beans (cy≈1-10) overflow upward via
SVG overflow:visible, which is the correct paw-print anatomy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- feed.getFeed: when Redis is unavailable, fall back to a direct
Postgres query (followed pets + own posts, cursor-based) instead of
returning an empty array — feed now works in dev without Upstash
- ProfileTabs: wrap grid thumbnails in <button> + open a bottom Sheet
with the full PostCard on tap; previously tapping a photo did nothing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MultiImageUpload: move onKeysChange out of setImages updater into
useEffect to fix React "Cannot update a component while rendering"
warning; same fix applied to handleRemove
- feed.getFeed: wrap getFeedPage in try/catch so Redis failure
(Upstash not configured) returns empty feed instead of 500
- posts.create + reposts.create: wrap fanOutPost with .catch(() => {})
so Redis failure never rolls back an already-committed post
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Next.js static rendering requires useSearchParams() to be inside a
Suspense boundary. Splits page into ResetPasswordContent (inner) +
ResetPasswordPage (Suspense wrapper). Caught by npm run build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>