Unify author display on the discovery-page cards
Groups, Pledges, and Campaigns each shipped a different author treatment in their card footers: groups used a raw <img> with no fallback, campaigns had no avatar at all, and pledges localized the 'by X' string while the others left it as a hardcoded English literal. Extract AuthorByline as the canonical author element. It uses the shadcn Avatar primitive (initials fallback), resolves the display name through the centralized getDisplayName helper, and links to the author's profile via useProfileUrl. The 'by Name' label is sourced from the shared common.byAuthor i18n key so every surface ships the same translated string in every locale. Inside a card, the byline renders as a <button> that navigates and stops propagation so the outer card <Link> keeps wrapping the whole card without nesting <a> inside <a>. CampaignCard also picks up a localized donor count via the new common.donors plural key, replacing the inline English 'donor' / 'donors' ternary.
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { CalendarClock, EyeOff, HandHeart, MapPin, ShieldCheck } from 'lucide-react';
|
||||
|
||||
import { AuthorByline } from '@/components/AuthorByline';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { CampaignModerationMenu } from '@/components/CampaignModerationMenu';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useBtcPrice } from '@/hooks/useBtcPrice';
|
||||
import { useCampaignDonations } from '@/hooks/useCampaignDonations';
|
||||
import { useCampaignModeration } from '@/hooks/useCampaignModeration';
|
||||
@@ -20,7 +21,6 @@ import {
|
||||
parseCampaign,
|
||||
} from '@/lib/campaign';
|
||||
import { formatCampaignAmount, formatUsdGoal, satsToUsd } from '@/lib/formatCampaignAmount';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -142,22 +142,18 @@ interface CampaignCardProps {
|
||||
* `<Link>` to the campaign's naddr-based detail route.
|
||||
*/
|
||||
export function CampaignCard({ campaign, variant = 'compact', className, footerBadge }: CampaignCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const { translatedEvent, translateAction } = useEventTranslation(campaign.event, {
|
||||
iconOnly: true,
|
||||
buttonClassName: 'size-8 rounded-full p-0 text-muted-foreground hover:text-primary hover:bg-primary/10',
|
||||
});
|
||||
const displayCampaign = parseCampaign(translatedEvent) ?? campaign;
|
||||
const author = useAuthor(campaign.pubkey);
|
||||
const { data: stats } = useCampaignDonations(campaign);
|
||||
const { data: btcPrice } = useBtcPrice();
|
||||
const { data: moderation } = useCampaignModeration();
|
||||
|
||||
const naddr = useMemo(() => encodeCampaignNaddr(campaign), [campaign]);
|
||||
const cover = sanitizeUrl(displayCampaign.banner);
|
||||
const creatorName =
|
||||
author.data?.metadata?.display_name ||
|
||||
author.data?.metadata?.name ||
|
||||
genUserName(campaign.pubkey);
|
||||
const deadline = campaign.deadline ? formatDeadline(campaign.deadline) : null;
|
||||
const raisedSats = stats?.totalSats ?? 0;
|
||||
const countryLabel = getCampaignCountryLabel(campaign);
|
||||
@@ -303,11 +299,11 @@ export function CampaignCard({ campaign, variant = 'compact', className, footerB
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between gap-3 border-t border-border/60 pt-3 text-xs text-muted-foreground">
|
||||
<div className="truncate">
|
||||
by <span className="font-medium text-foreground">{creatorName}</span>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<AuthorByline pubkey={campaign.pubkey} insideLink />
|
||||
{!isSilentPayment && stats && stats.donorCount > 0 && (
|
||||
<span className="ml-2 text-muted-foreground/80">
|
||||
· {stats.donorCount} {stats.donorCount === 1 ? 'donor' : 'donors'}
|
||||
<span className="shrink-0 text-muted-foreground/80">
|
||||
· {t('common.donors', { count: stats.donorCount })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,11 @@ import { Link } from 'react-router-dom';
|
||||
import { Users } from 'lucide-react';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
|
||||
import { AuthorByline } from '@/components/AuthorByline';
|
||||
import { CommunityModerationOverlay } from '@/components/CommunityModerationMenu';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { useEventTranslation } from '@/hooks/useEventTranslation';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
@@ -55,13 +54,7 @@ export function CommunityMiniCard({ community, className }: CommunityMiniCardPro
|
||||
buttonClassName: 'size-8 rounded-full p-0 text-muted-foreground hover:text-primary hover:bg-primary/10',
|
||||
});
|
||||
const displayCommunity = parseCommunityEvent(translatedEvent) ?? community;
|
||||
const founder = useAuthor(community.founderPubkey);
|
||||
const banner = sanitizeUrl(displayCommunity.image);
|
||||
const founderName =
|
||||
founder.data?.metadata?.display_name ||
|
||||
founder.data?.metadata?.name ||
|
||||
genUserName(community.founderPubkey);
|
||||
const founderAvatar = sanitizeUrl(founder.data?.metadata?.picture);
|
||||
|
||||
const naddr = nip19.naddrEncode({
|
||||
kind: COMMUNITY_DEFINITION_KIND,
|
||||
@@ -107,21 +100,7 @@ export function CommunityMiniCard({ community, className }: CommunityMiniCardPro
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-auto flex items-center justify-between gap-2 pt-1.5">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{founderAvatar ? (
|
||||
<img
|
||||
src={founderAvatar}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
className="size-5 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-5 rounded-full bg-secondary" />
|
||||
)}
|
||||
<span className="truncate text-[11px] text-muted-foreground">
|
||||
by {founderName}
|
||||
</span>
|
||||
</div>
|
||||
<AuthorByline pubkey={community.founderPubkey} insideLink />
|
||||
{translateAction}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user