diff --git a/frontend/app/components/PostCard/PostCard.tsx b/frontend/app/components/PostCard/PostCard.tsx index 3ea9479..5fda081 100644 --- a/frontend/app/components/PostCard/PostCard.tsx +++ b/frontend/app/components/PostCard/PostCard.tsx @@ -1,242 +1,119 @@ 'use client'; -import { useState, useEffect } from 'react'; -import CommentModal from '../CommentModal/CommentModal'; +import { useRef } from 'react'; -type Comment = { +export type PostCardData = { id: number; - author: string; + petName: string; + petType: 'Hund' | 'Katze'; + authorName: string; + createdAt: string; text: string; -}; - -type Media = { - type: 'image' | 'video'; - url: string; - aspect: '1:1' | '9:16'; + likesCount: number; + commentsCount: number; + }; type PostCardProps = { - petName: string; - petType: 'Hund' | 'Katze'; - ownerName?: string; - text: string; - createdAt: string; - media?: Media; - likes: number; - likedByCurrentUser?: boolean; - commentsCount: number; - commentsList: Comment[]; + post: PostCardData; + isDiscovery?: boolean + onOpenPreview: (post: PostCardData) => void; + onCommentClick?: (post: PostCardData) => void; + + }; -// 🔐 Simulierter eingeloggter User -const CURRENT_USER_ID = 1; - export default function PostCard({ - petName, - petType, - ownerName, - text, - createdAt, - media, - likes, - likedByCurrentUser, - commentsCount, - commentsList, + post, + onOpenPreview, + onCommentClick, + isDiscovery, }: PostCardProps) { - const [showComments, setShowComments] = useState(false); - const [showMediaFullscreen, setShowMediaFullscreen] = useState(false); + const likeRef = useRef(null); + const commentRef = useRef(null); - const [localLikes, setLocalLikes] = useState(likes); - const [liked, setLiked] = useState(!!likedByCurrentUser); - - const isSquare = media?.aspect === '1:1'; - - // ⛔ Scroll sperren bei Vollbild - useEffect(() => { - if (showMediaFullscreen) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = ''; - } - }, [showMediaFullscreen]); + function animate( + ref: React.RefObject, + animation: string + ) { + if (!ref.current) return; + const el = ref.current; + el.style.animation = animation; + el.addEventListener( + 'animationend', + () => { + el.style.animation = ''; + }, + { once: true } + ); + } return ( - <> -
+ + {/* Klickbarer Bereich → Preview */} +
onOpenPreview(post)} > - {/* HEADER */} -
-
- {petName}{' '} - - ({petType}) - -
+
-
- {createdAt} - {ownerName && ( - <> - {' · '} - - verwaltet von {ownerName} - - - )} -
-
+
+
+ {isDiscovery && ( +
+ +
+ )} - {/* MEDIA */} - {media && ( -
setShowMediaFullscreen(true)} - style={{ - width: '100%', - aspectRatio: isSquare ? '1 / 1' : '9 / 16', - backgroundColor: '#000', - cursor: 'pointer', - }} - > - {media.type === 'image' && ( - {petName} - )} +
+ + {post.petName} + + + · {post.petType} · {post.authorName} · {post.createdAt} + +
+
- {media.type === 'video' && ( -
- )} - - {/* TEXT */} -
- {text} +

{post.text}

+
- {/* FOOTER */} -
+ - console.log('LIKE', { - byUser: CURRENT_USER_ID, - pet: petName, - }); - }} - style={{ - background: 'none', - border: 'none', - cursor: 'pointer', - fontWeight: 600, - color: liked ? '#d32f2f' : '#555', - }} - > - ❤️ {localLikes} - - - -
-
- - {/* 💬 Kommentare */} - {showComments && ( - setShowComments(false)} - /> - )} - - {/* 🖼️ MEDIA FULLSCREEN */} - {showMediaFullscreen && media && ( -
setShowMediaFullscreen(false)} - style={{ - position: 'fixed', - inset: 0, - backgroundColor: 'rgba(0,0,0,0.9)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - zIndex: 2000, - cursor: 'zoom-out', +
- )} - + 💬 + {post.commentsCount} + + + ); } diff --git a/frontend/app/explore/page.tsx b/frontend/app/explore/page.tsx index 89fa045..f776b8d 100644 --- a/frontend/app/explore/page.tsx +++ b/frontend/app/explore/page.tsx @@ -1,26 +1,15 @@ 'use client'; import { useRef, useState } from 'react'; +import PostCard, { + PostCardData, +} from '@/app/components/PostCard/PostCard'; -type ExplorePet = { - id: number; - name: string; - type: 'Hund' | 'Katze'; -}; +/* ======================= + MOCK DATA +======================= */ -type ExplorePost = { - id: number; - petId: number; - petName: string; - petType: 'Hund' | 'Katze'; - authorName: string; - createdAt: string; - text: string; - likesCount: number; - commentsCount: number; -}; - -const trendingPets: ExplorePet[] = [ +const trendingPets = [ { id: 1, name: 'Luna', type: 'Katze' }, { id: 2, name: 'Bello', type: 'Hund' }, { id: 3, name: 'Milo', type: 'Katze' }, @@ -29,10 +18,9 @@ const trendingPets: ExplorePet[] = [ { id: 6, name: 'Leo', type: 'Hund' }, ]; -const latestPosts: ExplorePost[] = [ +const latestPosts: PostCardData[] = [ { id: 101, - petId: 1, petName: 'Luna', petType: 'Katze', authorName: 'Anna', @@ -43,7 +31,6 @@ const latestPosts: ExplorePost[] = [ }, { id: 102, - petId: 2, petName: 'Bello', petType: 'Hund', authorName: 'Markus', @@ -54,7 +41,6 @@ const latestPosts: ExplorePost[] = [ }, { id: 103, - petId: 3, petName: 'Milo', petType: 'Katze', authorName: 'Sophie', @@ -65,7 +51,6 @@ const latestPosts: ExplorePost[] = [ }, { id: 104, - petId: 4, petName: 'Rocky', petType: 'Hund', authorName: 'Timo', @@ -76,15 +61,20 @@ const latestPosts: ExplorePost[] = [ }, ]; +/* ======================= + PAGE +======================= */ + export default function ExplorePage() { - const [activeFilter, setActiveFilter] = useState< - 'ALLE' | 'HUND' | 'KATZE' - >('ALLE'); + const [activeFilter, setActiveFilter] = + useState<'ALLE' | 'HUND' | 'KATZE'>('ALLE'); - const [previewPost, setPreviewPost] = useState(null); -const [openComments, setOpenComments] = useState(false); + const [previewPost, setPreviewPost] = + useState(null); - // Swipe-Down Handling fürs Modal + const [openComments, setOpenComments] = useState(false); + + // Swipe-Down fürs Modal const startY = useRef(null); const filteredPosts = latestPosts.filter((p) => { @@ -96,90 +86,46 @@ const [openComments, setOpenComments] = useState(false); return ( <> {/* ================= HEADER / FILTER ================= */} -
+
-

Entdecken

-

- Neue Tiere, Trends & besondere Momente -

+

Entdecken

+

Neue Tiere, Trends & besondere Momente

-
+
{[ { key: 'ALLE', label: 'Alle' }, { key: 'HUND', label: 'Hunde' }, { key: 'KATZE', label: 'Katzen' }, - ].map((f) => { - const active = activeFilter === f.key; - return ( - - ); - })} + ].map((f) => ( + + ))}
{/* ================= BELIEBTE TIERE ================= */} -
- - Beliebte Tiere - +
+ Beliebte Tiere -
+
{trendingPets.map((pet) => ( -
-
- 🐾 -
-
+
+
🐾
+
{pet.name} -
{pet.type}
+ {pet.type}
))} @@ -187,151 +133,23 @@ const [openComments, setOpenComments] = useState(false);
{/* ================= POSTS ================= */} -
- - Neueste Beiträge - +
+ Neueste Beiträge -
+
{filteredPosts.map((post) => ( -
{ + setOpenComments(false); + setPreviewPost(p); }} - > - {/* ✅ Nur dieser Bereich öffnet das Modal */} -
setPreviewPost(post)} - style={{ cursor: 'pointer' }} - > -
- -
-
-
- {post.petName}{' '} - - · {post.petType} · {post.authorName} - -
- - {post.createdAt} - -
- -

{post.text}

-
-
- - {/* ✅ Interaktionszeile (öffnet KEIN Modal) */} -
- - - - - - -
-
+ onCommentClick={(p) => { + setOpenComments(true); + setPreviewPost(p); + }} + /> ))}
@@ -339,127 +157,63 @@ const [openComments, setOpenComments] = useState(false); {/* ================= PREVIEW MODAL ================= */} {previewPost && (
setPreviewPost(null)} - style={{ - position: 'fixed', - inset: 0, - backgroundColor: 'rgba(0,0,0,0.4)', - zIndex: 50, - display: 'flex', - justifyContent: 'center', - alignItems: 'flex-end', + className="post-preview-overlay" + onClick={() => { + setPreviewPost(null); + setOpenComments(false); }} >
e.stopPropagation()} - onTouchStart={(e) => (startY.current = e.touches[0].clientY)} + onTouchStart={(e) => + (startY.current = e.touches[0].clientY) + } onTouchEnd={(e) => { if ( startY.current && e.changedTouches[0].clientY - startY.current > 80 ) { setPreviewPost(null); -setOpenComments(false); - + setOpenComments(false); } }} - style={{ - backgroundColor: 'white', - width: '100%', - maxWidth: '720px', - borderTopLeftRadius: '22px', - borderTopRightRadius: '22px', - padding: '1rem', - animation: 'slideUp 0.25s ease', - paddingBottom: '5rem', - - }} > -
+
-
+
{previewPost.petName}{' '} - + · {previewPost.petType} · {previewPost.authorName} -

{previewPost.text}

-{openComments && ( -
- Kommentare +

{previewPost.text}

-
- Noch keine Kommentare. -
+
+ ❤️ {previewPost.likesCount} + 💬 {previewPost.commentsCount} +
- -
-)} + {openComments && ( +
+ Kommentare +

+ Noch keine Kommentare. +

-
-
- ❤️ {previewPost.likesCount} - 💬 {previewPost.commentsCount} +
+ )} +
Zum Beitrag diff --git a/frontend/app/feed/page.tsx b/frontend/app/feed/page.tsx index 18f1dc7..183863e 100644 --- a/frontend/app/feed/page.tsx +++ b/frontend/app/feed/page.tsx @@ -1,179 +1,237 @@ 'use client'; -import { useState } from 'react'; -import PostCard from '../components/PostCard/PostCard'; -import FreshPostsSlider from '../components/FreshPostsSlider/FreshPostsSlider'; +import { useRef, useState } from 'react'; +import PostCard, { + PostCardData, +} from '@/app/components/PostCard/PostCard'; -/* ---------------- TYPES ---------------- */ +/* ======================= + MOCK: USER KONTEXT +======================= */ -type Comment = { - id: number; - author: string; - text: string; -}; - -type Media = { - type: 'image' | 'video'; - url: string; - aspect: '1:1' | '9:16'; -}; - -type FeedPost = { - id: number; - - // 🐾 Tier (Absender) - petId: number; - petName: string; - petType: 'Hund' | 'Katze'; - - // 👤 Admin (sekundär) - ownerId: number; - ownerName: string; - - // 📝 Inhalt - text: string; - createdAt: string; - - // 📸 / 🎥 Media - media?: Media; - - // ❤️ Interaktion - likes: number; - //likedByCurrentUser: number; // ✅ FEHLTE - commentsCount: number; - commentsList: Comment[]; -}; - -type FeedAdmin = { - id: number; - name: string; - avatar: string; -}; - -/* ---------------- MOCK ADMINS ---------------- */ - -const mockAdmins: FeedAdmin[] = [ +const myPets = [ { id: 1, - name: 'Anna', - avatar: 'https://i.pravatar.cc/100?img=12', - }, - { - id: 2, - name: 'Markus', - avatar: 'https://i.pravatar.cc/100?img=32', + petType: 'Katze', + breed: 'Bengal', + ageMonths: 24, }, ]; -/* ---------------- MOCK POSTS ---------------- */ +const followedPetIds = [1, 3, 5]; -const mockPosts: FeedPost[] = [ +/* ======================= + MOCK: POSTS +======================= */ + +type FeedPost = PostCardData & { + petId: number; + breed: string; + ageMonths: number; +}; + +const allPosts: FeedPost[] = [ { - id: 1, + id: 401, petId: 1, petName: 'Luna', petType: 'Katze', - - ownerId: 1, - ownerName: 'Anna', - - text: 'Luna genießt die Sonne auf dem Balkon ☀️', - createdAt: 'vor 2 Stunden', - - media: { - type: 'image', - url: 'https://placekitten.com/600/600', - aspect: '1:1', - }, - - likes: 12, + breed: 'Bengal', + ageMonths: 26, + authorName: 'Anna', + createdAt: 'vor 10 Minuten', + text: 'Luna liebt den neuen Kratzbaum.', + likesCount: 8, commentsCount: 2, - commentsList: [ - { id: 1, author: 'Lisa', text: 'Wie süß 😍' }, - { id: 2, author: 'Tom', text: 'Typisch Katze!' }, - ], }, { - id: 2, + id: 402, petId: 2, - petName: 'Bello', + petName: 'Rocky', petType: 'Hund', - - ownerId: 2, - ownerName: 'Markus', - - text: 'Spaziergang im Park – jemand hatte sehr viel Energie 🐕', - createdAt: 'heute Morgen', - - media: { - type: 'video', - url: 'https://www.w3schools.com/html/mov_bbb.mp4', - aspect: '9:16', - }, - - likes: 20, - - - commentsCount: 0, - commentsList: [], + breed: 'Labrador', + ageMonths: 30, + authorName: 'Tim', + createdAt: 'vor 1 Stunde', + text: 'Rocky war schwimmen 🐕‍🦺', + likesCount: 15, + commentsCount: 4, + }, + { + id: 403, + petId: 3, + petName: 'Milo', + petType: 'Katze', + breed: 'Bengal', + ageMonths: 22, + authorName: 'Sophie', + createdAt: 'heute', + text: 'Milo entdeckt den Balkon.', + likesCount: 11, + commentsCount: 3, + }, + { + id: 404, + petId: 4, + petName: 'Nala', + petType: 'Katze', + breed: 'Maine Coon', + ageMonths: 40, + authorName: 'Laura', + createdAt: 'gestern', + text: 'Nala beobachtet alles ganz ruhig.', + likesCount: 6, + commentsCount: 1, }, ]; -/* ---------------- PAGE ---------------- */ +/* ======================= + RELEVANZ LOGIK +======================= */ + +function relevanceScore(post: FeedPost) { + let score = 0; + + myPets.forEach((pet) => { + if (post.petType === pet.petType) score += 3; + if (post.breed === pet.breed) score += 5; + if (Math.abs(post.ageMonths - pet.ageMonths) <= 6) score += 2; + }); + + score += Math.min(post.likesCount / 10, 3); + score += Math.min(post.commentsCount / 5, 2); + + return score; +} + +/* ======================= + PAGE +======================= */ export default function FeedPage() { - const [selectedOwnerId, setSelectedOwnerId] = useState(null); + const [previewPost, setPreviewPost] = + useState(null); + const [openComments, setOpenComments] = useState(false); - const visiblePosts = selectedOwnerId - ? mockPosts.filter((post) => post.ownerId === selectedOwnerId) - : mockPosts; + const startY = useRef(null); + + /* ======================= + FEED ZUSAMMENSTELLEN + ======================= */ + + const subscribedPosts = allPosts.filter((post) => + followedPetIds.includes(post.petId) + ); + + const discoveryPosts = allPosts + .filter((post) => !followedPetIds.includes(post.petId)) + .sort((a, b) => relevanceScore(b) - relevanceScore(a)); + + const FEED_SIZE = 20; + const DISCOVERY_RATIO = 0.15; + + const discoveryCount = Math.max( + 1, + Math.round(FEED_SIZE * DISCOVERY_RATIO) + ); + + const finalFeed = [ + ...subscribedPosts + .slice(0, FEED_SIZE - discoveryCount) + .map((p) => ({ ...p, isDiscovery: false })), + + ...discoveryPosts + .slice(0, discoveryCount) + .map((p) => ({ ...p, isDiscovery: true })), + ]; return ( -
- {/* HEADER */} -
-

Feed

-

- Neueste Beiträge aus der OnlyPets-Community -

-
- - {/* 🔄 Frische Tier-Admins Slider */} - - - {/* 📰 FEED */} -
- {visiblePosts.length === 0 && ( -

- Keine Beiträge von diesen Tier-Admins. -

- )} - - {visiblePosts.map((post) => ( - - ))} + <> + {/* ================= HEADER ================= */} +
+
+

Feed

+

Beiträge deiner Tiere & Abos

+
-
+ + {/* ================= POSTS ================= */} +
+
+ {finalFeed.map((post) => ( + { + setOpenComments(false); + setPreviewPost(p); + }} + onCommentClick={(p) => { + setOpenComments(true); + setPreviewPost(p); + }} + /> + ))} +
+
+ + {/* ================= PREVIEW MODAL ================= */} + {previewPost && ( +
{ + setPreviewPost(null); + setOpenComments(false); + }} + > +
e.stopPropagation()} + onTouchStart={(e) => + (startY.current = e.touches[0].clientY) + } + onTouchEnd={(e) => { + if ( + startY.current && + e.changedTouches[0].clientY - startY.current > 80 + ) { + setPreviewPost(null); + setOpenComments(false); + } + }} + > +
+
+ + {previewPost.petName}{' '} + + · {previewPost.petType} · {previewPost.authorName} + + +

{previewPost.text}

+ +
+ ❤️ {previewPost.likesCount} + 💬 {previewPost.commentsCount} +
+ + {openComments && ( +
+ Kommentare +

+ Noch keine Kommentare. +

+ +
+ )} +
+
+ )} + ); } diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 17bb253..100628a 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1,16 +1,49 @@ @import "tailwindcss"; +* { + box-sizing: border-box; +} :root { - --background: #ffffff; - --foreground: #171717; + /* ===== COLORS ===== */ + --color-primary: #2e7d32; + --color-primary-soft: #e6efe9; + + --color-bg: #f6faf7; + --color-surface: #ffffff; + + --color-text-main: #1f2d24; + --color-text-secondary: #5f6f66; + --color-text-muted: #8a9b92; + + --color-border: rgba(0, 0, 0, 0.06); + + /* ===== SHADOWS ===== */ + --shadow-sm: 0 2px 6px rgba(0, 0, 0, 0.05); + --shadow-md: 0 8px 20px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 20px 40px rgba(0, 0, 0, 0.12); + + /* ===== RADIUS ===== */ + --radius-sm: 10px; + --radius-md: 14px; + --radius-lg: 18px; + + /* ===== SPACING ===== */ + --space-xs: 0.25rem; + --space-sm: 0.5rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2rem; +} +html, body { + background: var(--color-bg); + color: var(--color-text-main); + font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + line-height: 1.5; } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} + + + @media (prefers-color-scheme: dark) { :root { @@ -96,4 +129,154 @@ body { transform: translateY(0); } } +.postcard { + background: var(--color-surface); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-sm); + padding: var(--space-md); + display: flex; + flex-direction: column; + gap: var(--space-sm); + transition: box-shadow 0.2s ease, transform 0.2s ease; +} +.postcard:hover { + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} +.postcard { + background: var(--color-surface); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-sm); + padding: var(--space-md); + display: flex; + flex-direction: column; + gap: var(--space-sm); + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +.postcard:hover { + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} +.postcard-content { + font-size: 0.95rem; + color: var(--color-text-secondary); +} +.postcard-actions { + display: flex; + gap: var(--space-md); + align-items: center; + font-size: 0.85rem; + color: var(--color-text-muted); +} + +.postcard-action { + display: flex; + align-items: center; + gap: 0.3rem; + cursor: pointer; + user-select: none; +} + +.postcard-action:hover { + color: var(--color-text-main); +} +.postcard--discovery { + border: 1px solid var(--color-primary); + box-shadow: + 0 0 0 1px rgba(46, 125, 50, 0.15), + var(--shadow-sm); +} +.feed-header, +.explore-header { + padding: var(--space-lg) var(--space-md) var(--space-md); +} + +.feed-header p, +.explore-header p { + color: var(--color-text-secondary); + margin: 0.25rem 0 0; +} +.post-list { + display: flex; + flex-direction: column; + gap: var(--space-md); + padding: 0 var(--space-md) var(--space-xl); +} +.fresh-scroll, +.trending-scroll { + display: flex; + gap: var(--space-sm); + overflow-x: auto; + padding: var(--space-sm) var(--space-md); +} + +.hide-scrollbar { + scrollbar-width: none; +} + +.hide-scrollbar::-webkit-scrollbar { + display: none; +} +.fresh-item, +.trending-item { + background: var(--color-surface); + border-radius: var(--radius-md); + padding: var(--space-sm); + min-width: 80px; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.3rem; + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); +} +.profile-header { + display: flex; + gap: var(--space-lg); + padding: var(--space-lg); + background: var(--color-surface); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-sm); + margin: var(--space-md); +} +.profile-stats { + display: flex; + gap: var(--space-lg); + margin-top: var(--space-sm); +} + +.profile-stats div { + text-align: center; +} + +.profile-stats strong { + font-size: 1.1rem; +} + +.profile-stats span { + font-size: 0.8rem; + color: var(--color-text-muted); +} +.post-preview-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.35); + display: flex; + justify-content: center; + align-items: flex-end; + z-index: 50; +} + +.post-preview-modal { + background: var(--color-surface); + width: 100%; + max-width: 600px; + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + padding: var(--space-lg); + box-shadow: var(--shadow-lg); +} diff --git a/frontend/app/profile/page.tsx b/frontend/app/profile/page.tsx index 9a9f03b..5ebdc90 100644 --- a/frontend/app/profile/page.tsx +++ b/frontend/app/profile/page.tsx @@ -1,3 +1,157 @@ +'use client'; + +import PostCard, { + PostCardData, +} from '@/app/components/PostCard/PostCard'; + +/* ======================= + MOCK: USER +======================= */ + +const user = { + id: 1, + name: 'Laura', + username: '@laura_pets', + bio: 'Liebt Tiere, Natur & ruhige Momente.', + stats: { + pets: 2, + posts: 6, + follows: 18, + }, +}; + +/* ======================= + MOCK: PETS +======================= */ + +const pets = [ + { + id: 1, + name: 'Nala', + type: 'Katze', + breed: 'Maine Coon', + age: '3 Jahre', + }, + { + id: 2, + name: 'Rocky', + type: 'Hund', + breed: 'Labrador', + age: '4 Jahre', + }, +]; + +/* ======================= + MOCK: POSTS +======================= */ + +const posts: PostCardData[] = [ + { + id: 501, + petName: 'Nala', + petType: 'Katze', + authorName: 'Laura', + createdAt: 'gestern', + text: 'Nala beobachtet alles ganz ruhig.', + likesCount: 6, + commentsCount: 1, + }, + { + id: 502, + petName: 'Rocky', + petType: 'Hund', + authorName: 'Laura', + createdAt: 'vor 2 Tagen', + text: 'Rocky liebt lange Spaziergänge.', + likesCount: 9, + commentsCount: 2, + }, +]; + +/* ======================= + PAGE +======================= */ + export default function ProfilePage() { - return

Profil

; + return ( + <> + {/* ================= USER HEADER ================= */} +
+
+ 👤 +
+ +
+

{user.name}

+ + {user.username} + + + {user.bio && ( +

+ {user.bio} +

+ )} + +
+
+ {user.stats.pets} + Tiere +
+
+ {user.stats.posts} + Beiträge +
+
+ {user.stats.follows} + Abos +
+
+
+
+ + {/* ================= PETS ================= */} +
+

Meine Tiere

+ + +
+ + {/* ================= POSTS ================= */} +
+

Beiträge

+ +
+ {posts.map((post) => ( + + ))} +
+
+ + ); }