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 (
-