From c7a09c6a1fdb37ccfb573a29d6fb67d2072c77e7 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Wed, 24 Dec 2025 14:38:24 +0100 Subject: [PATCH] feat(explore): stabilize explore UI, modal interactions & animations --- .../app/components/BottomNav/BottomNav.tsx | 179 +++++++ frontend/app/components/Navbar/Navbar.tsx | 47 ++ frontend/app/explore/page.tsx | 471 +++++++++++++++++- frontend/app/globals.css | 50 ++ frontend/app/layout.tsx | 16 +- frontend/middleware.ts | 42 ++ 6 files changed, 798 insertions(+), 7 deletions(-) create mode 100644 frontend/app/components/BottomNav/BottomNav.tsx create mode 100644 frontend/app/components/Navbar/Navbar.tsx create mode 100644 frontend/middleware.ts diff --git a/frontend/app/components/BottomNav/BottomNav.tsx b/frontend/app/components/BottomNav/BottomNav.tsx new file mode 100644 index 0000000..3114f0d --- /dev/null +++ b/frontend/app/components/BottomNav/BottomNav.tsx @@ -0,0 +1,179 @@ +'use client'; + +import Link from 'next/link'; +import { useEffect, useState } from 'react'; +import { usePathname } from 'next/navigation'; + +const leftItems = [ + { href: '/feed', label: 'Feed', icon: '🏠' }, + { href: '/explore', label: 'Entdecken', icon: '🔍' }, +]; + +const rightItems = [ + { href: '/pets', label: 'Tiere', icon: '🐾' }, + { href: '/profile', label: 'Profil', icon: '👤' }, +]; + +function NavItem({ + item, + pathname, +}: { + item: { href: string; label: string; icon: string }; + pathname: string; +}) { + const isActive = + item.href === '/feed' + ? pathname === '/feed' + : pathname.startsWith(item.href); + + return ( + { + (e.currentTarget as HTMLElement).style.transform = 'scale(0.9)'; +}} +onMouseUp={(e) => { + (e.currentTarget as HTMLElement).style.transform = 'scale(1)'; +}} +onMouseLeave={(e) => { + (e.currentTarget as HTMLElement).style.transform = 'scale(1)'; +}} + + > + {item.icon} + {item.label} + {isActive && ( + +)} + + + ); +} + +export default function BottomNav() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const pathname = usePathname(); + + useEffect(() => { + const hasAuthCookie = document.cookie + .split('; ') + .some((c) => c.startsWith('onlypets-auth=true')); + + setIsLoggedIn(hasAuthCookie); + }, []); + + if (!isLoggedIn) return null; + + return ( + + + ); +} diff --git a/frontend/app/components/Navbar/Navbar.tsx b/frontend/app/components/Navbar/Navbar.tsx new file mode 100644 index 0000000..5291177 --- /dev/null +++ b/frontend/app/components/Navbar/Navbar.tsx @@ -0,0 +1,47 @@ +'use client'; + +import Link from 'next/link'; +import { useEffect, useState } from 'react'; + +export default function Navbar() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + + useEffect(() => { + const hasAuthCookie = document.cookie + .split('; ') + .some((c) => c.startsWith('onlypets-auth=true')); + + setIsLoggedIn(hasAuthCookie); + }, []); + + return ( + + ); +} diff --git a/frontend/app/explore/page.tsx b/frontend/app/explore/page.tsx index ec5338f..89fa045 100644 --- a/frontend/app/explore/page.tsx +++ b/frontend/app/explore/page.tsx @@ -1,3 +1,472 @@ +'use client'; + +import { useRef, useState } from 'react'; + +type ExplorePet = { + id: number; + name: string; + type: 'Hund' | 'Katze'; +}; + +type ExplorePost = { + id: number; + petId: number; + petName: string; + petType: 'Hund' | 'Katze'; + authorName: string; + createdAt: string; + text: string; + likesCount: number; + commentsCount: number; +}; + +const trendingPets: ExplorePet[] = [ + { id: 1, name: 'Luna', type: 'Katze' }, + { id: 2, name: 'Bello', type: 'Hund' }, + { id: 3, name: 'Milo', type: 'Katze' }, + { id: 4, name: 'Rocky', type: 'Hund' }, + { id: 5, name: 'Nala', type: 'Katze' }, + { id: 6, name: 'Leo', type: 'Hund' }, +]; + +const latestPosts: ExplorePost[] = [ + { + id: 101, + petId: 1, + petName: 'Luna', + petType: 'Katze', + authorName: 'Anna', + createdAt: 'vor 2 Stunden', + text: 'Luna chillt auf dem Sofa und beobachtet alles ganz genau.', + likesCount: 12, + commentsCount: 3, + }, + { + id: 102, + petId: 2, + petName: 'Bello', + petType: 'Hund', + authorName: 'Markus', + createdAt: 'heute', + text: 'Bello hat sein neues Lieblingsspielzeug gefunden.', + likesCount: 25, + commentsCount: 8, + }, + { + id: 103, + petId: 3, + petName: 'Milo', + petType: 'Katze', + authorName: 'Sophie', + createdAt: 'gestern', + text: 'Milo hat wieder einen Karton erobert.', + likesCount: 7, + commentsCount: 1, + }, + { + id: 104, + petId: 4, + petName: 'Rocky', + petType: 'Hund', + authorName: 'Timo', + createdAt: 'gestern', + text: 'Rocky war im Wald unterwegs. 10/10 würde wieder rennen.', + likesCount: 19, + commentsCount: 4, + }, +]; + export default function ExplorePage() { - return

Entdecken

; + const [activeFilter, setActiveFilter] = useState< + 'ALLE' | 'HUND' | 'KATZE' + >('ALLE'); + + const [previewPost, setPreviewPost] = useState(null); +const [openComments, setOpenComments] = useState(false); + + // Swipe-Down Handling fürs Modal + const startY = useRef(null); + + const filteredPosts = latestPosts.filter((p) => { + if (activeFilter === 'HUND') return p.petType === 'Hund'; + if (activeFilter === 'KATZE') return p.petType === 'Katze'; + return true; + }); + + return ( + <> + {/* ================= HEADER / FILTER ================= */} +
+
+

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 ( + + ); + })} +
+
+ + {/* ================= BELIEBTE TIERE ================= */} +
+ + Beliebte Tiere + + +
+ {trendingPets.map((pet) => ( +
+
+ 🐾 +
+
+ {pet.name} +
{pet.type}
+
+
+ ))} +
+
+ + {/* ================= POSTS ================= */} +
+ + Neueste Beiträge + + +
+ {filteredPosts.map((post) => ( +
+ {/* ✅ 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) */} +
+ + + + + + +
+
+ ))} +
+
+ + {/* ================= 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', + }} + > +
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); + + } + }} + 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 + +
+ Noch keine Kommentare. +
+ + +
+)} + +
+
+ ❤️ {previewPost.likesCount} + 💬 {previewPost.commentsCount} +
+ + + Zum Beitrag + +
+
+
+ )} + + ); } diff --git a/frontend/app/globals.css b/frontend/app/globals.css index e41a4ba..17bb253 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -46,4 +46,54 @@ body { .navbar a.active { background-color: rgba(255, 255, 255, 0.35); } +/* Hide horizontal scrollbar but keep scroll */ +.hide-scrollbar { + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE / Edge */ +} + +.hide-scrollbar::-webkit-scrollbar { + display: none; /* Chrome / Safari */ +} +@keyframes skeleton { + 0% { background-position: 100% 0; } + 100% { background-position: 0 0; } +} +@keyframes heart-pop { + 0% { + transform: scale(1); + } + 40% { + transform: scale(1.25); + } + 70% { + transform: scale(0.95); + } + 100% { + transform: scale(1); + } +} + +@keyframes comment-wiggle { + 0% { + transform: translateX(0); + } + 30% { + transform: translateX(-2px); + } + 60% { + transform: translateX(2px); + } + 100% { + transform: translateX(0); + } +} +@keyframes slideUp { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index f723f9b..bb87584 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,5 +1,7 @@ import './globals.css'; -import Navigation from './components/Navigation'; +/*import Navigation from './components/Navigation';*/ +import BottomNav from './components/BottomNav/BottomNav'; + export const metadata = { title: 'OnlyPets', @@ -14,11 +16,13 @@ export default function RootLayout({ return ( - -
- {children} -
- +
+ {children} +
+ + + + ); } diff --git a/frontend/middleware.ts b/frontend/middleware.ts new file mode 100644 index 0000000..95ac474 --- /dev/null +++ b/frontend/middleware.ts @@ -0,0 +1,42 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + // 🔐 Simulierter Login-Check + // später: Cookie / Session / JWT + const isLoggedIn = request.cookies.get('onlypets-auth')?.value === 'true'; + + // 🚪 Gast auf Startseite lassen + if (!isLoggedIn && pathname === '/') { + return NextResponse.next(); + } + + // 🏠 Eingeloggt? Startseite überspringen → Feed + if (isLoggedIn && pathname === '/') { + return NextResponse.redirect(new URL('/feed', request.url)); + } + + // 🔒 Geschützte Seiten + const protectedRoutes = [ + '/feed', + '/post', + '/pets', + '/explore', // 🔍 Entdecken / Neue Beiträge + '/profile', // 👤 Eigenes Profil + '/user', // 👥 fremde Profile (z. B. /user/123) +]; + + + if (!isLoggedIn && protectedRoutes.some((route) => pathname.startsWith(route))) { + return NextResponse.redirect(new URL('/login', request.url)); + } + + // 🔄 Login-Seite für eingeloggte User blocken + if (isLoggedIn && pathname === '/login') { + return NextResponse.redirect(new URL('/feed', request.url)); + } + + return NextResponse.next(); +}