diff --git a/messages/de.json b/messages/de.json index dd8d71f..4afd8b3 100644 --- a/messages/de.json +++ b/messages/de.json @@ -10,7 +10,9 @@ "newPost": "Beitrag erstellen", "addPet": "Tier hinzufügen", "account": "Konto", - "profile": "Profil" + "profile": "Profil", + "collapseSidebar": "Sidebar einklappen", + "expandSidebar": "Sidebar ausklappen" }, "PostType": { "title": "Erstellen", @@ -102,7 +104,12 @@ "photoUpdated": "Profilfoto aktualisiert", "fileTooLarge": "Das Foto muss kleiner als 10 MB sein.", "invalidType": "Lade ein JPG-, PNG- oder WebP-Bild hoch.", - "uploading": "Wird hochgeladen…" + "uploading": "Wird hochgeladen…", + "cropTitle": "Foto zuschneiden", + "zoom": "Zoom", + "cropCancel": "Abbrechen", + "cropSave": "Übernehmen", + "cropSaving": "Wird verarbeitet…" }, "Onboarding": { "addFirstPet": "Füge dein erstes Tier hinzu", diff --git a/messages/en.json b/messages/en.json index bb2edf9..1381a20 100644 --- a/messages/en.json +++ b/messages/en.json @@ -10,7 +10,9 @@ "newPost": "New Post", "addPet": "Add Pet", "account": "Account", - "profile": "Profile" + "profile": "Profile", + "collapseSidebar": "Collapse sidebar", + "expandSidebar": "Expand sidebar" }, "PostType": { "title": "Create", @@ -102,7 +104,12 @@ "photoUpdated": "Profile photo updated", "fileTooLarge": "Photo must be under 10 MB.", "invalidType": "Upload a JPG, PNG, or WebP image.", - "uploading": "Uploading…" + "uploading": "Uploading…", + "cropTitle": "Crop photo", + "zoom": "Zoom", + "cropCancel": "Cancel", + "cropSave": "Apply", + "cropSaving": "Processing…" }, "Onboarding": { "addFirstPet": "Add your first pet", diff --git a/src/components/feed/PostCard.tsx b/src/components/feed/PostCard.tsx index 1c4b259..321c648 100644 --- a/src/components/feed/PostCard.tsx +++ b/src/components/feed/PostCard.tsx @@ -6,7 +6,7 @@ import { toast } from "sonner"; import { useMutation } from "@tanstack/react-query"; import { useTRPC } from "@/trpc/client"; import { useActivePet } from "@/context/ActivePetContext"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { PetAvatar } from "@/components/ui/pet-avatar"; import { BlurImage } from "@/components/ui/blur-image"; import { Carousel, @@ -36,7 +36,6 @@ import { import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import Link from "next/link"; -import { getAvatarUrl } from "@/lib/avatar-url"; import { getMediaUrl } from "@/lib/media-url"; import { parseCaption } from "@/lib/parse-caption"; import { formatRelativeVerbose } from "@/lib/format-relative-time"; @@ -158,7 +157,6 @@ function PostCardImpl({ post, onDeleted, variant = "feed" }: PostCardProps) { // Ownership: active pet's own post vs another pet's post const isOwnPost = activePetId === pet.id; - const avatarUrl = getAvatarUrl(pet.avatarKey); const isCarousel = images.length > 1; // Carousel: track current slide index. Subscribing to the embla API and @@ -237,14 +235,7 @@ function PostCardImpl({ post, onDeleted, variant = "feed" }: PostCardProps) { onClick={(e) => e.stopPropagation()} > - - {avatarUrl && ( - - )} - - {pet.name[0]?.toUpperCase() ?? "?"} - - +
diff --git a/src/components/feed/PostDetailDialog.tsx b/src/components/feed/PostDetailDialog.tsx index 38d73bc..fc8dbbc 100644 --- a/src/components/feed/PostDetailDialog.tsx +++ b/src/components/feed/PostDetailDialog.tsx @@ -7,7 +7,8 @@ import { toast } from "sonner"; import { useTRPC } from "@/trpc/client"; import { useActivePet } from "@/context/ActivePetContext"; import { Dialog, DialogContent } from "@/components/ui/dialog"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { PetAvatar } from "@/components/ui/pet-avatar"; import { Button } from "@/components/ui/button"; import { MentionTextarea } from "@/components/ui/mention-textarea"; import { @@ -36,7 +37,6 @@ import { type CarouselApi, } from "@/components/ui/carousel"; import Link from "next/link"; -import { getAvatarUrl } from "@/lib/avatar-url"; import { getMediaUrl } from "@/lib/media-url"; import { BlurImage } from "@/components/ui/blur-image"; import { parseCaption } from "@/lib/parse-caption"; @@ -178,7 +178,6 @@ export function PostDetailDialog({ post, open, onClose, onDeleted }: PostDetailD const { pet, images } = post; const isCarousel = images.length > 1; const isOwnPost = activePetId === pet.id; - const avatarUrl = getAvatarUrl(pet.avatarKey); const comments = commentsData?.comments ?? []; // Extracted so it renders identically in the mobile bar and the desktop right-panel @@ -190,12 +189,7 @@ export function PostDetailDialog({ post, open, onClose, onDeleted }: PostDetailD onClick={onClose} > - - {avatarUrl && } - - {pet.name[0]?.toUpperCase() ?? "?"} - - +

{pet.name}

@@ -407,16 +401,14 @@ export function PostDetailDialog({ post, open, onClose, onDeleted }: PostDetailD

{tc("empty")}

)} {comments.map((comment) => { - const commentAvatarUrl = getAvatarUrl(comment.pet.avatarKey); const isOwnComment = comment.pet.id === activePetId; return (
- - {commentAvatarUrl && } - - {comment.pet.name[0]?.toUpperCase()} - - +
- - {activePet.avatarKey && ( - - )} - - {activePet.name.slice(0, 2).toUpperCase()} - - + {activePet.name} {activePet.isVerified && ( @@ -120,17 +113,11 @@ export function MobileNav({ pets }: MobileNavProps) { onClick={() => setActivePet(pet.id)} className="flex items-center gap-2 cursor-pointer" > - - {pet.avatarKey && ( - - )} - - {pet.name.slice(0, 2).toUpperCase()} - - + {pet.name} {pet.isVerified && ( diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 7df110a..d34a9ea 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { @@ -12,6 +12,8 @@ import { MessageCircle, PawPrint, ChevronDown, + ChevronLeft, + ChevronRight, Plus, Ticket, BadgeCheck, @@ -28,10 +30,9 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { PetAvatar } from "@/components/ui/pet-avatar"; import { Badge } from "@/components/ui/badge"; import { useActivePet } from "@/context/ActivePetContext"; -import { getAvatarUrl } from "@/lib/avatar-url"; type Pet = { id: string; @@ -72,6 +73,8 @@ const navLinks = [ * DropdownMenuTrigger or DropdownMenuItem. Using onClick+useRouter for navigation. * - Clerk 7 UserButton no longer accepts afterSignOutUrl (removed from UserButtonProps). */ +const SIDEBAR_COLLAPSED_KEY = "sidebar_collapsed"; + export function Sidebar({ pets }: SidebarProps) { const pathname = usePathname(); const router = useRouter(); @@ -80,16 +83,49 @@ export function Sidebar({ pets }: SidebarProps) { const activePet = pets.find((p) => p.id === activePetId) ?? pets[0]; const [postTypeSheetOpen, setPostTypeSheetOpen] = useState(false); + const [collapsed, setCollapsed] = useState(false); // First-load fallback is handled once, centrally, by ActivePetInitializer // (mounted alongside Sidebar/MobileNav in the app layout) — don't duplicate it here. + useEffect(() => { + // Initialize from localStorage on client mount (avoids SSR mismatch — + // same accepted pattern as ActivePetContext). + const stored = localStorage.getItem(SIDEBAR_COLLAPSED_KEY); + if (stored === "true") setCollapsed(true); + }, []); + + const toggleCollapsed = () => { + setCollapsed((prev) => { + const next = !prev; + localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(next)); + return next; + }); + }; + return ( -