feat: Sprint 2 — i18n Search+Messages, Mention-Dropdown, Story-Tray, Explore Grid, Legal-Links
- Search-Seite vollständig übersetzt (Search-Namespace in EN+DE) - Messages-Listenseite übersetzt (pageTitle, noMessages, you, relativeTime etc.) - @-Mention Dropdown: Avatar 6→9, zwei-zeilig Name+Tierart, orange Fallback - StoryTray: -mx-6 entfernt, Add-Story-Button jetzt korrekt ausgerichtet - ExploreCard: lange Rassenamen umbrechen statt aus Grid herausdrängen - Impressum/Datenschutz/AGB-Links in Profil-Seite (/pets) ergänzt
This commit is contained in:
1 parent
68503e8915
commit
31602b08eb
9 files changed
+104
-48
No files matched your search
+13
-13
@@ -1,30 +1,30 @@
|
||||
Debug:
|
||||
|
||||
\-Die Suchen seite ist noch nicht auf Deutsch übersetzt.
|
||||
~~\-Die Suchen seite ist noch nicht auf Deutsch übersetzt.~~ ✅ ERLEDIGT
|
||||
|
||||
\-Die Erwähnfunktion mittels @ ist impelmentiert, aber es fehlt die Benachrichtung das man Erwähnt wurde ! Zudem ist die Auswahl nach dem tippen @ schlecht designed man kann gerade so den Avatar erkennen aber nicht den namen in der aufploppenden suche, bitte um FIX !
|
||||
\-Die Erwähnfunktion mittels @ ist impelmentiert, aber es fehlt die Benachrichtung das man Erwähnt wurde ! ~~Zudem ist die Auswahl nach dem tippen @ schlecht designed man kann gerade so den Avatar erkennen aber nicht den namen in der aufploppenden suche, bitte um FIX !~~ ✅ ERLEDIGT (Dropdown-Design)
|
||||
|
||||
\-Messages ist noch nicht auf Deutsch übersetzt.
|
||||
~~\-Messages ist noch nicht auf Deutsch übersetzt.~~ ✅ ERLEDIGT
|
||||
|
||||
\-Kommentar Update; wenn man einen Kommentar postet, muss dieser zwingend sofort im Kommentar-Verlauf sichtbar werden (Optische Bestätigung das der Kommentar angekommen ist für den Nutzer), es muss sich als Nutzer Live anfühlen auch wenn der Server noch am arbeiten ist.
|
||||
~~\-Kommentar Update; wenn man einen Kommentar postet, muss dieser zwingend sofort im Kommentar-Verlauf sichtbar werden (Optische Bestätigung das der Kommentar angekommen ist für den Nutzer), es muss sich als Nutzer Live anfühlen auch wenn der Server noch am arbeiten ist.~~ ✅ ERLEDIGT
|
||||
|
||||
\-Benachrichtung wenn man "Paw-Back this post" nutzt für den Original-User des Posts.
|
||||
|
||||
\-Die Mobil Ansicht ist noch nicht ausgereift, das Menü ist teilweise nicht mehr erkennbar im unteren Segemet ( Startseite, Entdecken, +, Benachrichtigungen, Profil).
|
||||
~~\-Die Mobil Ansicht ist noch nicht ausgereift, das Menü ist teilweise nicht mehr erkennbar im unteren Segemet ( Startseite, Entdecken, +, Benachrichtigungen, Profil).~~ ✅ ERLEDIGT
|
||||
|
||||
\-Mobilansicht - Add Story Button ist sehr linksbündig, keine Aufteilung vorhanden.
|
||||
~~\-Mobilansicht - Add Story Button ist sehr linksbündig, keine Aufteilung vorhanden.~~ ✅ ERLEDIGT
|
||||
|
||||
\-Mobilansicht - Kein PawFeed Logo sichtbar, soll nur auf der Startseite erscheinen beim scrollen verschwinden.
|
||||
\-Mobilansicht - Kein PawFeed Logo sichtbar, soll nur auf der Startseite erscheinen beim scrollen verschwinden.
|
||||
|
||||
\-Mobilansicht - wenn man bis man hin und her scrollt soll man bei langem hochscrollen einen refresh auslösen.
|
||||
\-Mobilansicht - wenn man bis man hin und her scrollt soll man bei langem hochscrollen einen refresh auslösen.
|
||||
|
||||
\-Mobilansicht - Impressum/Datenschutz in das Profil rein legen, damit man es erreichen kann.
|
||||
~~\-Mobilansicht - Impressum/Datenschutz in das Profil rein legen, damit man es erreichen kann.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Mobilansicht - Nachrichten nicht aufrufbar weil kein Button vorhanden. kann gerne als Mail-Icon mitscrollend oben rechts in die ecke damit es immer erreichbar ist.~~ ✅ ERLEDIGT
|
||||
~~-Mobilansicht - Nachrichten nicht aufrufbar weil kein Button vorhanden. kann gerne als Mail-Icon mitscrollend oben rechts in die ecke damit es immer erreichbar ist.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Mobilansicht - Explore Grid - bei langen Rassenamen drückt der Rassenamen aus dem Grid heraus. Anpassen entweder Grid auf 2Reihig oder Rassenamen werden mit Absatz angezeigt.~~ ✅ ERLEDIGT
|
||||
|
||||
|
||||
|
||||
\-User Delete Funktion, für den Unwahrscheinlichen fall ein User möchte sein Konto löschen wird ein Workflow benötigt, der nach einer Bestätigung mittels Tiernamen Eingabe (Auswahl eins von möglichen) den Kompletten Account löscht, sowie alle Hochgeladen Bilder/Videos und erzeugten Posts, Rückstandslos.
|
||||
|
||||
|
||||
\-User Delete Funktion, für den Unwahrscheinlichen fall ein User möchte sein Konto löschen wird ein Workflow benötigt, der nach einer Bestätigung mittels Tiernamen Eingabe (Auswahl eins von möglichen) den Kompletten Account löscht, sowie alle Hochgeladen Bilder/Videos und erzeugten Posts, Rückstandslos.
|
||||
|
||||
@@ -150,7 +150,29 @@
|
||||
"nowFollowing": "Du folgst jetzt {name}",
|
||||
"unfollowed": "{name} entfolgt"
|
||||
},
|
||||
"Search": {
|
||||
"title": "Suchen",
|
||||
"placeholder": "Tiere, Personen oder Hashtags suchen…",
|
||||
"tabPets": "Tiere",
|
||||
"tabPeople": "Personen",
|
||||
"tabTags": "Tags",
|
||||
"typeMore": "Mindestens 2 Zeichen eingeben",
|
||||
"typeHashtag": "Hashtag eingeben zum Suchen",
|
||||
"noPets": "Keine Tiere gefunden für \"{query}\"",
|
||||
"noPeople": "Keine Personen gefunden für \"{query}\"",
|
||||
"noTags": "Keine Beiträge mit #{tag}",
|
||||
"followerCount": "{count} Follower"
|
||||
},
|
||||
"Messages": {
|
||||
"pageTitle": "Nachrichten",
|
||||
"noMessages": "Noch keine Nachrichten",
|
||||
"noMessagesHint": "Besuche ein Tierprofil und tippe auf Nachricht, um ein Gespräch zu starten.",
|
||||
"you": "Du",
|
||||
"startConversation": "Gespräch starten",
|
||||
"timeNow": "jetzt",
|
||||
"relativeMinutes": "{count} Min.",
|
||||
"relativeHours": "{count} Std.",
|
||||
"relativeDays": "{count} T.",
|
||||
"today": "Heute",
|
||||
"yesterday": "Gestern",
|
||||
"empty": "Sag Hallo 👋",
|
||||
|
||||
@@ -150,7 +150,29 @@
|
||||
"nowFollowing": "You're now following {name}",
|
||||
"unfollowed": "Unfollowed {name}"
|
||||
},
|
||||
"Search": {
|
||||
"title": "Search",
|
||||
"placeholder": "Search pets, people or hashtags…",
|
||||
"tabPets": "Pets",
|
||||
"tabPeople": "People",
|
||||
"tabTags": "Tags",
|
||||
"typeMore": "Type at least 2 characters to search",
|
||||
"typeHashtag": "Type a hashtag to search",
|
||||
"noPets": "No pets found for \"{query}\"",
|
||||
"noPeople": "No people found for \"{query}\"",
|
||||
"noTags": "No posts tagged #{tag}",
|
||||
"followerCount": "{count} followers"
|
||||
},
|
||||
"Messages": {
|
||||
"pageTitle": "Messages",
|
||||
"noMessages": "No messages yet",
|
||||
"noMessagesHint": "Visit a pet's profile and tap Message to start a conversation.",
|
||||
"you": "You",
|
||||
"startConversation": "Start the conversation",
|
||||
"timeNow": "now",
|
||||
"relativeMinutes": "{count}m",
|
||||
"relativeHours": "{count}h",
|
||||
"relativeDays": "{count}d",
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"empty": "Say hello 👋",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { MessageCircle, Loader2 } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { useActivePet } from "@/context/ActivePetContext";
|
||||
@@ -9,19 +10,22 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { getAvatarUrl } from "@/lib/avatar-url";
|
||||
|
||||
function formatRelativeTime(date: Date | string): string {
|
||||
type TFunc = (key: string, opts?: Record<string, string | number>) => string;
|
||||
|
||||
function formatRelativeTime(date: Date | string, t: TFunc): 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 "now";
|
||||
if (diffMins < 60) return `${diffMins}m`;
|
||||
if (diffHours < 24) return `${diffHours}h`;
|
||||
if (diffDays < 7) return `${diffDays}d`;
|
||||
if (diffMins < 1) return t("timeNow");
|
||||
if (diffMins < 60) return t("relativeMinutes", { count: diffMins });
|
||||
if (diffHours < 24) return t("relativeHours", { count: diffHours });
|
||||
if (diffDays < 7) return t("relativeDays", { count: diffDays });
|
||||
return new Date(date).toLocaleDateString();
|
||||
}
|
||||
|
||||
export default function MessagesPage() {
|
||||
const t = useTranslations("Messages");
|
||||
const trpc = useTRPC();
|
||||
const router = useRouter();
|
||||
const { activePetId } = useActivePet();
|
||||
@@ -39,7 +43,7 @@ export default function MessagesPage() {
|
||||
<div className="px-4 py-5 border-b">
|
||||
<h1 className="text-xl font-semibold flex items-center gap-2">
|
||||
<MessageCircle className="h-5 w-5 text-orange-500" aria-hidden="true" />
|
||||
Messages
|
||||
{t("pageTitle")}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -52,10 +56,8 @@ export default function MessagesPage() {
|
||||
{!isLoading && conversations.length === 0 && (
|
||||
<div className="flex flex-col items-center gap-3 py-24 text-muted-foreground">
|
||||
<MessageCircle className="h-12 w-12 opacity-25" />
|
||||
<p className="text-sm font-medium">No messages yet</p>
|
||||
<p className="text-xs text-center max-w-[220px]">
|
||||
Visit a pet's profile and tap <strong>Message</strong> to start a conversation.
|
||||
</p>
|
||||
<p className="text-sm font-medium">{t("noMessages")}</p>
|
||||
<p className="text-xs text-center max-w-[220px]">{t("noMessagesHint")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -72,7 +74,6 @@ export default function MessagesPage() {
|
||||
onClick={() => router.push(`/messages/${conv.id}`)}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-muted/50 transition-colors text-left"
|
||||
>
|
||||
{/* Avatar */}
|
||||
<Avatar className="w-12 h-12 shrink-0">
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt={conv.otherPet.name} />}
|
||||
<AvatarFallback className="text-sm bg-muted">
|
||||
@@ -80,23 +81,22 @@ export default function MessagesPage() {
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<p className={`text-sm truncate ${hasUnread ? "font-semibold" : "font-medium"}`}>
|
||||
{conv.otherPet.name}
|
||||
</p>
|
||||
<span className="text-xs text-muted-foreground shrink-0">
|
||||
{formatRelativeTime(conv.updatedAt)}
|
||||
{formatRelativeTime(conv.updatedAt, t as TFunc)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 mt-0.5">
|
||||
<p className={`text-xs truncate ${hasUnread ? "text-foreground" : "text-muted-foreground"}`}>
|
||||
{conv.lastMessage
|
||||
? conv.lastMessage.senderPetId === activePetId
|
||||
? `You: ${conv.lastMessage.body}`
|
||||
? `${t("you")}: ${conv.lastMessage.body}`
|
||||
: conv.lastMessage.body
|
||||
: "Start the conversation"}
|
||||
: t("startConversation")}
|
||||
</p>
|
||||
{hasUnread && (
|
||||
<Badge className="shrink-0 bg-orange-500 text-white text-xs px-1.5 min-w-[20px] h-5 flex items-center justify-center rounded-full">
|
||||
|
||||
@@ -24,6 +24,14 @@ export default async function PetsPage() {
|
||||
</div>
|
||||
|
||||
<PetGrid pets={pets} />
|
||||
|
||||
<div className="mt-10 pt-4 border-t flex items-center justify-center gap-4 text-xs text-muted-foreground">
|
||||
<Link href="/impressum" className="hover:text-foreground transition-colors">Impressum</Link>
|
||||
<span aria-hidden="true">·</span>
|
||||
<Link href="/datenschutz" className="hover:text-foreground transition-colors">Datenschutz</Link>
|
||||
<span aria-hidden="true">·</span>
|
||||
<Link href="/nutzungsbedingungen" className="hover:text-foreground transition-colors">AGB</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Search, Layers2, Loader2, Users } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -15,6 +16,7 @@ import { getAvatarUrl } from "@/lib/avatar-url";
|
||||
import { getMediaUrl } from "@/lib/media-url";
|
||||
|
||||
export default function SearchPage() {
|
||||
const t = useTranslations("Search");
|
||||
const trpc = useTRPC();
|
||||
const { activePetId } = useActivePet();
|
||||
|
||||
@@ -54,12 +56,12 @@ export default function SearchPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-[640px] mx-auto px-4 py-6">
|
||||
<h1 className="text-xl font-semibold mb-4">Search</h1>
|
||||
<h1 className="text-xl font-semibold mb-4">{t("title")}</h1>
|
||||
|
||||
<div className="relative mb-4">
|
||||
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" aria-hidden="true" />
|
||||
<Input
|
||||
placeholder="Search pets, people or hashtags…"
|
||||
placeholder={t("placeholder")}
|
||||
value={query}
|
||||
onChange={handleQueryChange}
|
||||
className="pl-9"
|
||||
@@ -69,16 +71,16 @@ export default function SearchPage() {
|
||||
|
||||
<Tabs defaultValue="pets">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="pets">Pets</TabsTrigger>
|
||||
<TabsTrigger value="people">People</TabsTrigger>
|
||||
<TabsTrigger value="tags">Tags</TabsTrigger>
|
||||
<TabsTrigger value="pets">{t("tabPets")}</TabsTrigger>
|
||||
<TabsTrigger value="people">{t("tabPeople")}</TabsTrigger>
|
||||
<TabsTrigger value="tags">{t("tabTags")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Pets tab */}
|
||||
<TabsContent value="pets">
|
||||
{!petsEnabled ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-10">
|
||||
Type at least 2 characters to search
|
||||
{t("typeMore")}
|
||||
</p>
|
||||
) : loadingPets ? (
|
||||
<div className="flex justify-center py-10">
|
||||
@@ -86,7 +88,7 @@ export default function SearchPage() {
|
||||
</div>
|
||||
) : pets.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-10">
|
||||
No pets found for “{trimmed}”
|
||||
{t("noPets", { query: trimmed })}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -140,7 +142,7 @@ export default function SearchPage() {
|
||||
<TabsContent value="people">
|
||||
{!peopleEnabled ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-10">
|
||||
Type at least 2 characters to search
|
||||
{t("typeMore")}
|
||||
</p>
|
||||
) : loadingPeople ? (
|
||||
<div className="flex justify-center py-10">
|
||||
@@ -149,7 +151,7 @@ export default function SearchPage() {
|
||||
) : owners.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-2 py-10 text-muted-foreground">
|
||||
<Users className="h-8 w-8 opacity-30" />
|
||||
<p className="text-sm">No people found for “{trimmed}”</p>
|
||||
<p className="text-sm">{t("noPeople", { query: trimmed })}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-5">
|
||||
@@ -211,7 +213,7 @@ export default function SearchPage() {
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate">{pet.name}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{pet.breed?.name ?? pet.species.name} · {pet._count.followers} followers
|
||||
{pet.breed?.name ?? pet.species.name} · {t("followerCount", { count: pet._count.followers })}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -238,7 +240,7 @@ export default function SearchPage() {
|
||||
<TabsContent value="tags">
|
||||
{!tagsEnabled ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-10">
|
||||
Type a hashtag to search
|
||||
{t("typeHashtag")}
|
||||
</p>
|
||||
) : loadingTags ? (
|
||||
<div className="flex justify-center py-10">
|
||||
@@ -246,7 +248,7 @@ export default function SearchPage() {
|
||||
</div>
|
||||
) : tagPosts.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-10">
|
||||
No posts tagged #{normalizedTag}
|
||||
{t("noTags", { tag: normalizedTag })}
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-0.5 mt-0.5">
|
||||
|
||||
@@ -43,7 +43,7 @@ export function ExploreCard({ pet, activePetId }: ExploreCardProps) {
|
||||
</p>
|
||||
</Link>
|
||||
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<Badge variant="secondary" className="text-xs max-w-full text-center whitespace-normal break-words leading-tight">
|
||||
{labelName}
|
||||
</Badge>
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ export function StoryTray() {
|
||||
<>
|
||||
{/* Horizontal scroll tray */}
|
||||
<div
|
||||
className="flex gap-4 overflow-x-auto px-6 py-3 scrollbar-hide -mx-6"
|
||||
className="flex gap-4 overflow-x-auto px-6 py-3 scrollbar-hide"
|
||||
role="list"
|
||||
aria-label="Stories"
|
||||
>
|
||||
|
||||
@@ -122,27 +122,29 @@ export function MentionTextarea({
|
||||
className={className}
|
||||
/>
|
||||
{mention && pets.length > 0 && (
|
||||
<div className="absolute z-50 bottom-full mb-1 left-0 right-0 bg-card border rounded-md shadow-lg overflow-hidden max-h-48 overflow-y-auto">
|
||||
<div className="absolute z-50 bottom-full mb-1 left-0 right-0 bg-card border rounded-md shadow-lg overflow-hidden max-h-56 overflow-y-auto">
|
||||
{pets.map((pet) => (
|
||||
<button
|
||||
key={pet.id}
|
||||
type="button"
|
||||
className="w-full flex items-center gap-2 px-3 py-2 hover:bg-muted text-sm text-left"
|
||||
className="w-full flex items-center gap-3 px-3 py-2.5 hover:bg-muted transition-colors text-left"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
handleSelect(pet.name);
|
||||
}}
|
||||
>
|
||||
<Avatar className="h-6 w-6 shrink-0">
|
||||
<Avatar className="h-9 w-9 shrink-0">
|
||||
{pet.avatarKey && (
|
||||
<AvatarImage src={getAvatarUrl(pet.avatarKey)!} alt={pet.name} />
|
||||
)}
|
||||
<AvatarFallback className="text-xs bg-muted">
|
||||
<AvatarFallback className="text-xs font-semibold bg-orange-100 text-orange-700">
|
||||
{pet.name.slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="font-medium flex-1 truncate">{pet.name}</span>
|
||||
<span className="text-muted-foreground text-xs shrink-0">{pet.species.name}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-foreground truncate">{pet.name}</p>
|
||||
<p className="text-xs text-muted-foreground">{pet.species.name}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user