Files
eranos/src/components/CommunityContent.tsx
T
lemon ac3cdf34b2 Rename user-facing 'Community' → 'Organization'
User-visible copy now matches the Organization rebrand. Internal
symbols, file names, query keys, routes, and storage keys are
intentionally left alone for this pass — they're still pinned to
"community" / "communities" until a dedicated rename commit.

Touched strings:

- `MobileBottomNav` and `sidebarItems` labels: "Communities" →
  "Organize", matching the existing TopNav copy.
- `CommunityDetailPage` hero fallback ("Unnamed Community" →
  "Unnamed Organization") and "About this organization" aria-label.
- `CommunityContent` thumbnail fallback name.
- `ExternalContentHeader.CommunityPreview` row label and fallback name.
- `NoteCard` kind-34550 noun "community" → "organization" (used in
  feed-card action lines like "created an organization") and the
  article switches from "a" to "an".
- `NoteMoreMenu` overflow-menu labels: "Report post to community" →
  "Report post to organization", "Remove from community" → "Remove
  from organization".
- `BanConfirmDialog` title, description, and success/failure toasts.
- `CommunityContentWarning` reporter pluralization and the
  fallback report-type label ("community guidelines" →
  "organization guidelines"); reporters are now scoped to founder /
  moderators per the commit 4 cleanup, so the wording reflects that.
- `CommunityReportDialog` description copy.
- `CreateGoalDialog` placeholder example.
- `CreateActionDialog` org-scoped description string.
- `CreateCommunityEventDialog` NIP-31 `alt` tag prefix.
- `CommentContext` kind-34550 entries in the action-noun and
  rendered-noun maps ("a community" / "community" → "an
  organization" / "organization").
- `extraKinds` kind-34550 entry: label, description, and blurb.
- `kindLabels` kinds 4550, 10004, 34550.
- `DiscoverHero` ticker stat copy.
- `GetFeedTool` error message drops "communities" since the
  Following feed no longer includes organization activity (removed
  in the badge-runtime commit).
2026-05-20 12:32:57 -07:00

100 lines
3.4 KiB
TypeScript

import { useMemo } from 'react';
import { Users, Globe } from 'lucide-react';
import type { NostrEvent } from '@nostrify/nostrify';
import { sanitizeUrl } from '@/lib/sanitizeUrl';
// --- Helpers ---
function getTag(tags: string[][], name: string): string | undefined {
return tags.find(([n]) => n === name)?.[1];
}
function parseCommunityEvent(event: NostrEvent) {
const name = getTag(event.tags, 'name') || getTag(event.tags, 'd') || 'Unnamed Organization';
const description = getTag(event.tags, 'description') || '';
const image = getTag(event.tags, 'image');
// Extract moderators from p tags with "moderator" role
const moderators = event.tags
.filter(([n, , , role]) => n === 'p' && role === 'moderator')
.map(([, pubkey]) => pubkey)
.filter(Boolean);
// Extract relays
const relays = event.tags
.filter(([n]) => n === 'relay')
.map(([, url, marker]) => ({ url, marker }))
.filter((r) => !!r.url);
return { name, description, image, moderators, relays };
}
// --- Main Component ---
export function CommunityContent({ event }: { event: NostrEvent }) {
const { name, description, image } = useMemo(
() => parseCommunityEvent(event),
[event],
);
// Extract website URL from description if present
const descriptionUrl = useMemo(() => {
const urlMatch = description.match(/https?:\/\/[^\s]+/);
return sanitizeUrl(urlMatch?.[0]);
}, [description]);
// Description text without trailing URL (if the URL is the last thing)
const descriptionText = useMemo(() => {
if (!descriptionUrl) return description;
return description.replace(new RegExp(`\\s*${descriptionUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`), '').trim();
}, [description, descriptionUrl]);
return (
<div className="mt-3 space-y-5">
{/* Community hero image */}
{image ? (
<div className="relative -mx-4 aspect-[21/9] overflow-hidden">
<img
src={image}
alt={name}
className="w-full h-full object-cover"
/>
{/* Gradient overlay for text readability */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
{/* Community name overlaid on image */}
<div className="absolute bottom-0 left-0 right-0 px-4 pb-4">
<h1 className="text-2xl font-bold text-white leading-tight drop-shadow-lg">{name}</h1>
</div>
</div>
) : (
<div className="relative -mx-4 aspect-[21/9] bg-gradient-to-br from-primary/15 via-primary/5 to-transparent flex items-center justify-center">
<Users className="size-16 text-primary/20" />
<div className="absolute bottom-0 left-0 right-0 px-4 pb-4">
<h1 className="text-2xl font-bold leading-tight">{name}</h1>
</div>
</div>
)}
{/* Description */}
{descriptionText && (
<p className="text-sm leading-relaxed text-muted-foreground whitespace-pre-wrap">{descriptionText}</p>
)}
{/* Website link */}
{descriptionUrl && (
<a
href={descriptionUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-sm text-primary hover:underline"
>
<Globe className="size-3.5" />
{descriptionUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')}
</a>
)}
</div>
);
}