feat(i18n): translate FeedList, PostCard, CommentSheet, FollowButton, MessageThread
Adds PostCard, Comments, Follow, Messages, FeedList namespaces to en/de messages. Moves formatRelativeTime/formatDay into components so they can use the t() hook. All hardcoded strings replaced with next-intl calls. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
4b17537a9a
commit
376ea74b36
7 files changed
+201
-79
No files matched your search
@@ -102,5 +102,62 @@
|
||||
"switch": "Sprache",
|
||||
"en": "English",
|
||||
"de": "Deutsch"
|
||||
},
|
||||
"PostCard": {
|
||||
"justNow": "gerade eben",
|
||||
"minutesAgo": "vor {count} Min.",
|
||||
"hoursAgo": "vor {count} Std.",
|
||||
"daysAgo": "vor {count} T.",
|
||||
"editCaption": "Beschreibung bearbeiten",
|
||||
"deletePost": "Beitrag löschen",
|
||||
"reportPost": "Beitrag melden",
|
||||
"blockPet": "{name} blockieren",
|
||||
"captionPlaceholder": "Beschreibung eingeben…",
|
||||
"charsRemaining": "Noch {count} Zeichen",
|
||||
"saveCaption": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"more": "mehr",
|
||||
"less": "weniger",
|
||||
"deleteTitle": "Beitrag löschen?",
|
||||
"deleteDesc": "Dieser Beitrag und alle Fotos werden dauerhaft gelöscht. Das kann nicht rückgängig gemacht werden.",
|
||||
"keepPost": "Behalten",
|
||||
"yesDelete": "Ja, löschen",
|
||||
"captionUpdated": "Beschreibung aktualisiert",
|
||||
"captionUpdateError": "Beschreibung konnte nicht aktualisiert werden. Bitte versuche es erneut.",
|
||||
"postDeleted": "Beitrag gelöscht",
|
||||
"deleteError": "Beitrag konnte nicht gelöscht werden. Bitte versuche es erneut."
|
||||
},
|
||||
"Comments": {
|
||||
"title": "Kommentare",
|
||||
"justNow": "jetzt",
|
||||
"empty": "Noch keine Kommentare — sei der Erste!",
|
||||
"placeholder": "Kommentar hinzufügen…",
|
||||
"post": "Senden",
|
||||
"postError": "Kommentar konnte nicht gepostet werden. Bitte versuche es erneut.",
|
||||
"deleteError": "Kommentar konnte nicht gelöscht werden. Bitte versuche es erneut."
|
||||
},
|
||||
"Follow": {
|
||||
"follow": "Folgen",
|
||||
"following": "Gefolgt",
|
||||
"unfollow": "Entfolgen",
|
||||
"unfollowTitle": "{name} entfolgen?",
|
||||
"unfollowDesc": "Du siehst {name}'s Beiträge nicht mehr in deinem Feed.",
|
||||
"keepFollowing": "Weiter folgen",
|
||||
"followError": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
|
||||
"nowFollowing": "Du folgst jetzt {name}",
|
||||
"unfollowed": "{name} entfolgt"
|
||||
},
|
||||
"Messages": {
|
||||
"today": "Heute",
|
||||
"yesterday": "Gestern",
|
||||
"empty": "Sag Hallo 👋",
|
||||
"placeholder": "Nachricht…",
|
||||
"charsLeft": "Noch {count} Zeichen",
|
||||
"sendError": "Nachricht konnte nicht gesendet werden. Bitte versuche es erneut."
|
||||
},
|
||||
"FeedList": {
|
||||
"forYou": "Für dich",
|
||||
"recent": "Aktuell",
|
||||
"allCaughtUp": "Du bist auf dem neuesten Stand. Folge mehr Tieren für weitere Beiträge."
|
||||
}
|
||||
}
|
||||
@@ -102,5 +102,62 @@
|
||||
"switch": "Language",
|
||||
"en": "English",
|
||||
"de": "Deutsch"
|
||||
},
|
||||
"PostCard": {
|
||||
"justNow": "just now",
|
||||
"minutesAgo": "{count}m ago",
|
||||
"hoursAgo": "{count}h ago",
|
||||
"daysAgo": "{count}d ago",
|
||||
"editCaption": "Edit caption",
|
||||
"deletePost": "Delete post",
|
||||
"reportPost": "Report post",
|
||||
"blockPet": "Block {name}",
|
||||
"captionPlaceholder": "Write a caption…",
|
||||
"charsRemaining": "{count} characters remaining",
|
||||
"saveCaption": "Save caption",
|
||||
"cancel": "Cancel",
|
||||
"more": "more",
|
||||
"less": "less",
|
||||
"deleteTitle": "Delete this post?",
|
||||
"deleteDesc": "This will permanently delete the post and all its photos. This can't be undone.",
|
||||
"keepPost": "Keep post",
|
||||
"yesDelete": "Yes, delete",
|
||||
"captionUpdated": "Caption updated",
|
||||
"captionUpdateError": "Couldn't update caption. Please try again.",
|
||||
"postDeleted": "Post deleted",
|
||||
"deleteError": "Couldn't delete post. Please try again."
|
||||
},
|
||||
"Comments": {
|
||||
"title": "Comments",
|
||||
"justNow": "now",
|
||||
"empty": "No comments yet — be the first!",
|
||||
"placeholder": "Add a comment…",
|
||||
"post": "Post",
|
||||
"postError": "Couldn't post comment. Please try again.",
|
||||
"deleteError": "Couldn't delete comment. Please try again."
|
||||
},
|
||||
"Follow": {
|
||||
"follow": "Follow",
|
||||
"following": "Following",
|
||||
"unfollow": "Unfollow",
|
||||
"unfollowTitle": "Unfollow {name}?",
|
||||
"unfollowDesc": "You'll stop seeing {name}'s posts in your feed.",
|
||||
"keepFollowing": "Keep following",
|
||||
"followError": "Something went wrong. Try again.",
|
||||
"nowFollowing": "You're now following {name}",
|
||||
"unfollowed": "Unfollowed {name}"
|
||||
},
|
||||
"Messages": {
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"empty": "Say hello 👋",
|
||||
"placeholder": "Message…",
|
||||
"charsLeft": "{count} characters left",
|
||||
"sendError": "Failed to send message. Try again."
|
||||
},
|
||||
"FeedList": {
|
||||
"forYou": "For you",
|
||||
"recent": "Recent",
|
||||
"allCaughtUp": "You're all caught up. Follow more pets to see more posts."
|
||||
}
|
||||
}
|
||||
@@ -17,18 +17,7 @@ import {
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { getAvatarUrl } from "@/lib/avatar-url";
|
||||
|
||||
function formatRelativeTime(date: Date | string): string {
|
||||
const diffMs = Date.now() - new Date(date).getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
if (diffMins < 1) return "just now";
|
||||
if (diffMins < 60) return `${diffMins}m`;
|
||||
if (diffHours < 24) return `${diffHours}h`;
|
||||
if (diffDays < 7) return `${diffDays}d`;
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface CommentSheetProps {
|
||||
postId: string;
|
||||
@@ -38,7 +27,20 @@ interface CommentSheetProps {
|
||||
}
|
||||
|
||||
export function CommentSheet({ postId, petId, open, onOpenChange }: CommentSheetProps) {
|
||||
const t = useTranslations("Comments");
|
||||
const trpc = useTRPC();
|
||||
|
||||
function formatRelativeTime(date: Date | string): string {
|
||||
const diffMs = Date.now() - new Date(date).getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
if (diffMins < 1) return t("justNow");
|
||||
if (diffMins < 60) return `${diffMins}m`;
|
||||
if (diffHours < 24) return `${diffHours}h`;
|
||||
if (diffDays < 7) return `${diffDays}d`;
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
const queryClient = useQueryClient();
|
||||
const [body, setBody] = useState("");
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -59,7 +61,7 @@ export function CommentSheet({ postId, petId, open, onOpenChange }: CommentSheet
|
||||
queryKey: [["trpc", "feed", "getFeed"]],
|
||||
});
|
||||
},
|
||||
onError: () => toast.error("Couldn't post comment. Please try again."),
|
||||
onError: () => toast.error(t("postError")),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -73,7 +75,7 @@ export function CommentSheet({ postId, petId, open, onOpenChange }: CommentSheet
|
||||
queryKey: [["trpc", "feed", "getFeed"]],
|
||||
});
|
||||
},
|
||||
onError: () => toast.error("Couldn't delete comment. Please try again."),
|
||||
onError: () => toast.error(t("deleteError")),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -104,7 +106,7 @@ export function CommentSheet({ postId, petId, open, onOpenChange }: CommentSheet
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent side="bottom" className="h-[80vh] flex flex-col p-0">
|
||||
<SheetHeader className="px-4 pt-4 pb-2">
|
||||
<SheetTitle className="text-left">Comments</SheetTitle>
|
||||
<SheetTitle className="text-left">{t("title")}</SheetTitle>
|
||||
</SheetHeader>
|
||||
|
||||
<Separator />
|
||||
@@ -116,7 +118,7 @@ export function CommentSheet({ postId, petId, open, onOpenChange }: CommentSheet
|
||||
</div>
|
||||
) : comments.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-10">
|
||||
No comments yet — be the first!
|
||||
{t("empty")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="py-2">
|
||||
@@ -172,7 +174,7 @@ export function CommentSheet({ postId, petId, open, onOpenChange }: CommentSheet
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
maxLength={500}
|
||||
placeholder="Add a comment…"
|
||||
placeholder={t("placeholder")}
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -187,7 +189,7 @@ export function CommentSheet({ postId, petId, open, onOpenChange }: CommentSheet
|
||||
{createMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Post"
|
||||
t("post")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SkeletonCard } from "@/components/feed/SkeletonCard";
|
||||
import { EmptyFeed } from "@/components/feed/EmptyFeed";
|
||||
import { WelcomeCard } from "@/components/feed/WelcomeCard";
|
||||
import { Sparkles, Clock } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
const AD_INTERVAL = 7;
|
||||
|
||||
type FeedMode = "chrono" | "algo";
|
||||
@@ -37,6 +38,7 @@ export function FeedList() {
|
||||
const { activePetId } = useActivePet();
|
||||
const trpc = useTRPC();
|
||||
const { mode, changeMode } = useFeedMode();
|
||||
const t = useTranslations("FeedList");
|
||||
|
||||
const { ref: sentinelRef, inView } = useInView({ threshold: 0 });
|
||||
|
||||
@@ -111,7 +113,7 @@ export function FeedList() {
|
||||
}`}
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
Für dich
|
||||
{t("forYou")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => changeMode("chrono")}
|
||||
@@ -122,7 +124,7 @@ export function FeedList() {
|
||||
}`}
|
||||
>
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
Aktuell
|
||||
{t("recent")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +154,7 @@ export function FeedList() {
|
||||
|
||||
{!hasNextPage && !isFetchingNextPage && allPosts.length > 0 && (
|
||||
<p className="text-center text-sm text-muted-foreground py-8">
|
||||
{"You're all caught up. Follow more pets to see more posts."}
|
||||
{t("allCaughtUp")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import { CommentSheet } from "@/components/feed/CommentSheet";
|
||||
import { RepostCard } from "@/components/feed/RepostCard";
|
||||
import { BlockDialog } from "@/components/safety/BlockDialog";
|
||||
import { ReportSheet } from "@/components/safety/ReportSheet";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* PostCard — full photo post card with carousel, caption clamp, and options menu.
|
||||
@@ -117,25 +118,25 @@ export type PostCardProps = {
|
||||
variant?: "feed" | "detail";
|
||||
};
|
||||
|
||||
function formatRelativeTime(date: Date | string): string {
|
||||
const now = Date.now();
|
||||
const diffMs = now - new Date(date).getTime();
|
||||
const diffSecs = Math.floor(diffMs / 1000);
|
||||
const diffMins = Math.floor(diffSecs / 60);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffSecs < 60) return "just now";
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
|
||||
export function PostCard({ post, onDeleted, variant = "feed" }: PostCardProps) {
|
||||
const t = useTranslations("PostCard");
|
||||
const trpc = useTRPC();
|
||||
const { activePetId } = useActivePet();
|
||||
|
||||
function formatRelativeTime(date: Date | string): string {
|
||||
const now = Date.now();
|
||||
const diffMs = now - new Date(date).getTime();
|
||||
const diffSecs = Math.floor(diffMs / 1000);
|
||||
const diffMins = Math.floor(diffSecs / 60);
|
||||
const diffHours = Math.floor(diffMins / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
if (diffSecs < 60) return t("justNow");
|
||||
if (diffMins < 60) return t("minutesAgo", { count: diffMins });
|
||||
if (diffHours < 24) return t("hoursAgo", { count: diffHours });
|
||||
if (diffDays < 7) return t("daysAgo", { count: diffDays });
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
|
||||
const { pet, images, createdAt } = post;
|
||||
const [currentCaption, setCurrentCaption] = useState(post.caption);
|
||||
|
||||
@@ -183,10 +184,10 @@ export function PostCard({ post, onDeleted, variant = "feed" }: PostCardProps) {
|
||||
onSuccess: (updated) => {
|
||||
setCurrentCaption(updated.caption);
|
||||
setIsEditingCaption(false);
|
||||
toast.success("Caption updated");
|
||||
toast.success(t("captionUpdated"));
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Couldn't update caption. Please try again.");
|
||||
toast.error(t("captionUpdateError"));
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -195,11 +196,11 @@ export function PostCard({ post, onDeleted, variant = "feed" }: PostCardProps) {
|
||||
trpc.posts.delete.mutationOptions({
|
||||
onSuccess: () => {
|
||||
setShowDeleteDialog(false);
|
||||
toast.success("Post deleted");
|
||||
toast.success(t("postDeleted"));
|
||||
onDeleted?.(post.id);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Couldn't delete post. Please try again.");
|
||||
toast.error(t("deleteError"));
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -280,13 +281,13 @@ export function PostCard({ post, onDeleted, variant = "feed" }: PostCardProps) {
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Edit caption
|
||||
{t("editCaption")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
className="cursor-pointer text-destructive focus:text-destructive"
|
||||
>
|
||||
Delete post
|
||||
{t("deletePost")}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
) : (
|
||||
@@ -296,14 +297,14 @@ export function PostCard({ post, onDeleted, variant = "feed" }: PostCardProps) {
|
||||
onClick={() => setShowReportSheet(true)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
Report post
|
||||
{t("reportPost")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => setShowBlockDialog(true)}
|
||||
className="cursor-pointer text-destructive focus:text-destructive"
|
||||
>
|
||||
Block {pet.name}
|
||||
{t("blockPet", { name: pet.name })}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
@@ -386,7 +387,7 @@ export function PostCard({ post, onDeleted, variant = "feed" }: PostCardProps) {
|
||||
<Textarea
|
||||
value={editCaptionValue}
|
||||
onChange={(e) => setEditCaptionValue(e.target.value)}
|
||||
placeholder="Write a caption…"
|
||||
placeholder={t("captionPlaceholder")}
|
||||
maxLength={500}
|
||||
rows={3}
|
||||
className="text-sm resize-none"
|
||||
@@ -394,7 +395,7 @@ export function PostCard({ post, onDeleted, variant = "feed" }: PostCardProps) {
|
||||
/>
|
||||
{editCaptionValue.length > 450 && (
|
||||
<p className="text-xs text-muted-foreground text-right">
|
||||
{500 - editCaptionValue.length} characters remaining
|
||||
{t("charsRemaining", { count: 500 - editCaptionValue.length })}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
@@ -404,7 +405,7 @@ export function PostCard({ post, onDeleted, variant = "feed" }: PostCardProps) {
|
||||
disabled={updateCaptionMutation.isPending}
|
||||
className="bg-orange-500 hover:bg-orange-600 text-white text-xs h-8"
|
||||
>
|
||||
Save caption
|
||||
{t("saveCaption")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -413,7 +414,7 @@ export function PostCard({ post, onDeleted, variant = "feed" }: PostCardProps) {
|
||||
disabled={updateCaptionMutation.isPending}
|
||||
className="text-xs h-8"
|
||||
>
|
||||
Cancel
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -447,7 +448,7 @@ export function PostCard({ post, onDeleted, variant = "feed" }: PostCardProps) {
|
||||
onClick={() => setIsCaptionExpanded((v) => !v)}
|
||||
className="text-xs text-muted-foreground hover:text-foreground mt-0.5"
|
||||
>
|
||||
{isCaptionExpanded ? "less" : "more"}
|
||||
{isCaptionExpanded ? t("less") : t("more")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -483,20 +484,19 @@ export function PostCard({ post, onDeleted, variant = "feed" }: PostCardProps) {
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete this post?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("deleteTitle")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete the post and all its photos. This
|
||||
can't be undone.
|
||||
{t("deleteDesc")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Keep post</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("keepPost")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDelete}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Yes, delete
|
||||
{t("yesDelete")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -12,28 +12,30 @@ import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { getAvatarUrl } from "@/lib/avatar-url";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
function formatTime(date: Date | string) {
|
||||
return new Date(date).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function formatDay(date: Date | string) {
|
||||
const d = new Date(date);
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
if (d.toDateString() === today.toDateString()) return "Today";
|
||||
if (d.toDateString() === yesterday.toDateString()) return "Yesterday";
|
||||
return d.toLocaleDateString([], { weekday: "long", month: "short", day: "numeric" });
|
||||
}
|
||||
|
||||
interface MessageThreadProps {
|
||||
conversationId: string;
|
||||
otherPet: { id: string; name: string; avatarKey: string | null };
|
||||
}
|
||||
|
||||
export function MessageThread({ conversationId, otherPet }: MessageThreadProps) {
|
||||
const t = useTranslations("Messages");
|
||||
const trpc = useTRPC();
|
||||
|
||||
function formatDay(date: Date | string) {
|
||||
const d = new Date(date);
|
||||
const today = new Date();
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
if (d.toDateString() === today.toDateString()) return t("today");
|
||||
if (d.toDateString() === yesterday.toDateString()) return t("yesterday");
|
||||
return d.toLocaleDateString([], { weekday: "long", month: "short", day: "numeric" });
|
||||
}
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { activePetId } = useActivePet();
|
||||
@@ -64,7 +66,7 @@ export function MessageThread({ conversationId, otherPet }: MessageThreadProps)
|
||||
trpc.conversations.unreadCount.queryOptions({ petId: activePetId ?? "" })
|
||||
);
|
||||
},
|
||||
onError: () => toast.error("Failed to send message. Try again."),
|
||||
onError: () => toast.error(t("sendError")),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -147,7 +149,7 @@ export function MessageThread({ conversationId, otherPet }: MessageThreadProps)
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<p className="text-sm font-medium mt-2">{otherPet.name}</p>
|
||||
<p className="text-xs">Say hello 👋</p>
|
||||
<p className="text-xs">{t("empty")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -218,7 +220,7 @@ export function MessageThread({ conversationId, otherPet }: MessageThreadProps)
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Message…"
|
||||
placeholder={t("placeholder")}
|
||||
maxLength={1000}
|
||||
rows={1}
|
||||
className="resize-none text-sm min-h-[40px] max-h-[120px] flex-1"
|
||||
@@ -241,7 +243,7 @@ export function MessageThread({ conversationId, otherPet }: MessageThreadProps)
|
||||
</div>
|
||||
{body.length > 900 && (
|
||||
<p className="text-xs text-muted-foreground text-right mt-1">
|
||||
{1000 - body.length} characters left
|
||||
{t("charsLeft", { count: 1000 - body.length })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
/**
|
||||
* FollowButton — optimistic follow/unfollow toggle (SOCL-01, SOCL-02)
|
||||
@@ -51,6 +52,7 @@ export function FollowButton({
|
||||
followeePetId,
|
||||
followeeName,
|
||||
}: FollowButtonProps) {
|
||||
const t = useTranslations("Follow");
|
||||
const trpc = useTRPC();
|
||||
const queryClient = useQueryClient();
|
||||
const [showUnfollowDialog, setShowUnfollowDialog] = useState(false);
|
||||
@@ -84,13 +86,13 @@ export function FollowButton({
|
||||
isFollowingQueryOptions.queryKey,
|
||||
context?.prev
|
||||
);
|
||||
toast.error("Something went wrong. Try again.");
|
||||
toast.error(t("followError"));
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: isFollowingQueryOptions.queryKey });
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success(`You're now following ${followeeName}`);
|
||||
toast.success(t("nowFollowing", { name: followeeName }));
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -108,13 +110,13 @@ export function FollowButton({
|
||||
isFollowingQueryOptions.queryKey,
|
||||
context?.prev
|
||||
);
|
||||
toast.error("Something went wrong. Try again.");
|
||||
toast.error(t("followError"));
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: isFollowingQueryOptions.queryKey });
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast(`Unfollowed ${followeeName}`);
|
||||
toast(t("unfollowed", { name: followeeName }));
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -148,17 +150,17 @@ export function FollowButton({
|
||||
? "min-h-[44px] transition-colors duration-150"
|
||||
: "min-h-[44px] bg-orange-500 hover:bg-orange-600 text-white transition-colors duration-150"
|
||||
}
|
||||
aria-label={isFollowing ? `Unfollow ${followeeName}` : `Follow ${followeeName}`}
|
||||
aria-label={isFollowing ? `${t("unfollow")} ${followeeName}` : `${t("follow")} ${followeeName}`}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin opacity-60" />
|
||||
) : isFollowing ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
Following
|
||||
{t("following")}
|
||||
</>
|
||||
) : (
|
||||
"Follow"
|
||||
t("follow")
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -166,15 +168,15 @@ export function FollowButton({
|
||||
<AlertDialog open={showUnfollowDialog} onOpenChange={setShowUnfollowDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Unfollow {followeeName}?</AlertDialogTitle>
|
||||
<AlertDialogTitle>{t("unfollowTitle", { name: followeeName })}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You'll stop seeing {followeeName}'s posts in your feed.
|
||||
{t("unfollowDesc", { name: followeeName })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Keep following</AlertDialogCancel>
|
||||
<AlertDialogCancel>{t("keepFollowing")}</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleUnfollowConfirm}>
|
||||
Unfollow
|
||||
{t("unfollow")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
Reference in new issue
Block a user