Improve Zapstore and app handler card display
- Rename Zapstore kind labels to include 'Zapstore' prefix across all label registries (NoteCard, PostDetailPage, CommentContext, ExternalContentHeader, NotificationsPage, extraKinds) - Wrap Zapstore (32267, 30063, 3063) compact and detail content in rounded bordered cards with hover effects; remove redundant mt-2/mt-3 margins from component roots - Replace useLinkPreview thumbnail with metadata banner/picture in kind 31990 app handler cards (compact and full views) - Add pt-4 to Zapstore detail card wrappers in PostDetailPage - Fix sticky tab bar (SubHeaderBar z-10) being painted over by card content: remove z-10 from AppHandlerContent inner div and add z-0 to the main content column in MainLayout
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import type { NostrEvent, NostrMetadata } from '@nostrify/nostrify';
|
||||
import { ExternalLink, GitFork, Package } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ExternalFavicon } from '@/components/ExternalFavicon';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useLinkPreview } from '@/hooks/useLinkPreview';
|
||||
import { NostrURI } from '@/lib/NostrURI';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -79,42 +78,32 @@ export function AppHandlerContent({ event, compact }: AppHandlerContentProps) {
|
||||
const name = metadata.name || getTag(event.tags, 'name') || getTag(event.tags, 'd') || 'Unknown App';
|
||||
const about = metadata.about;
|
||||
const picture = metadata.picture;
|
||||
const banner = metadata.banner;
|
||||
const websiteUrl = getWebsiteUrl(event.tags, metadata);
|
||||
const hashtags = getAllTags(event.tags, 't');
|
||||
|
||||
const shakespeareUrl = useMemo(() => getShakespeareUrl(event.tags), [event.tags]);
|
||||
|
||||
const { data: preview, isLoading: previewLoading } = useLinkPreview(websiteUrl ?? null);
|
||||
const thumbnailUrl = preview?.thumbnail_url;
|
||||
|
||||
const [imgError, setImgError] = useState(false);
|
||||
const showThumbnail = thumbnailUrl && !imgError;
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<div className="rounded-xl border border-border overflow-hidden transition-all duration-300 hover:shadow-md hover:border-primary/20">
|
||||
{/* Screenshot hero — only shown while loading or when a thumbnail exists */}
|
||||
{(previewLoading || showThumbnail) && (
|
||||
{/* Banner hero */}
|
||||
{banner && (
|
||||
<div className="relative aspect-[2/1] bg-gradient-to-br from-muted/50 to-muted overflow-hidden">
|
||||
{previewLoading ? (
|
||||
<Skeleton className="absolute inset-0" />
|
||||
) : (
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt={name}
|
||||
className="size-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
onError={() => setImgError(true)}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
src={banner}
|
||||
alt=""
|
||||
className="size-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 px-3.5 pb-3.5 space-y-2">
|
||||
{/* App icon — overlaps the screenshot hero like a profile avatar */}
|
||||
<div className={showThumbnail || previewLoading ? '-mt-7' : 'pt-3.5'}>
|
||||
<div className="relative px-3.5 pb-3.5 space-y-2">
|
||||
{/* App icon — overlaps the banner hero like a profile avatar */}
|
||||
<div className={banner ? '-mt-7' : 'pt-3.5'}>
|
||||
{picture ? (
|
||||
<img
|
||||
src={picture}
|
||||
@@ -193,29 +182,24 @@ export function AppHandlerContent({ event, compact }: AppHandlerContentProps) {
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<div className="rounded-xl border border-border overflow-hidden">
|
||||
{/* Screenshot hero — only shown while loading or when a thumbnail exists */}
|
||||
{(previewLoading || showThumbnail) && (
|
||||
{/* Banner hero */}
|
||||
{banner && (
|
||||
<div className="relative aspect-[2/1] bg-gradient-to-br from-muted/50 to-muted overflow-hidden">
|
||||
{previewLoading ? (
|
||||
<Skeleton className="absolute inset-0" />
|
||||
) : (
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt={name}
|
||||
className="size-full object-cover"
|
||||
loading="lazy"
|
||||
onError={() => setImgError(true)}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
src={banner}
|
||||
alt=""
|
||||
className="size-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 px-4 pb-4 space-y-3">
|
||||
{/* App icon — overlaps the screenshot hero like a profile avatar */}
|
||||
<div className="relative px-4 pb-4 space-y-3">
|
||||
{/* App icon — overlaps the banner hero like a profile avatar */}
|
||||
<div className={cn(
|
||||
'flex items-end justify-between',
|
||||
showThumbnail || previewLoading ? '-mt-10' : 'pt-4',
|
||||
banner ? '-mt-10' : 'pt-4',
|
||||
)}>
|
||||
{picture ? (
|
||||
<img
|
||||
|
||||
@@ -108,8 +108,8 @@ const KIND_LABELS: Record<number, string> = {
|
||||
30030: 'an emoji pack',
|
||||
30054: 'a podcast episode',
|
||||
30055: 'a podcast trailer',
|
||||
3063: 'an asset',
|
||||
30063: 'a release',
|
||||
3063: 'a Zapstore asset',
|
||||
30063: 'a Zapstore release',
|
||||
30311: 'a stream',
|
||||
30315: 'a status',
|
||||
30617: 'a repository',
|
||||
@@ -117,7 +117,7 @@ const KIND_LABELS: Record<number, string> = {
|
||||
31922: 'a calendar event',
|
||||
31923: 'a calendar event',
|
||||
31990: 'an app',
|
||||
32267: 'an app',
|
||||
32267: 'a Zapstore app',
|
||||
34139: 'a playlist',
|
||||
34236: 'a divine',
|
||||
34550: 'a community',
|
||||
@@ -214,11 +214,11 @@ const KIND_SUFFIXES: Partial<Record<number, string>> = {
|
||||
34139: 'playlist',
|
||||
};
|
||||
|
||||
/** Postfix that replaces the default pattern (e.g. "Ditto on Zapstore" instead of "Ditto app"). */
|
||||
/** Postfix that replaces the default pattern (e.g. "Ditto on Zapstore" instead of "Ditto Zapstore app"). */
|
||||
const KIND_POSTFIXES: Partial<Record<number, string>> = {
|
||||
32267: 'on Zapstore',
|
||||
30063: 'release',
|
||||
3063: 'asset',
|
||||
30063: 'Zapstore release',
|
||||
3063: 'Zapstore asset',
|
||||
};
|
||||
|
||||
/** Get a display name for an event based on its kind and tags. */
|
||||
|
||||
@@ -1083,9 +1083,9 @@ function hasVideo(tags: string[][]): boolean {
|
||||
/** Fallback labels for well-known kinds not in EXTRA_KINDS. */
|
||||
const WELL_KNOWN_KIND_LABELS: Record<number, string> = {
|
||||
31990: 'App',
|
||||
32267: 'App',
|
||||
30063: 'Release',
|
||||
3063: 'Asset',
|
||||
32267: 'Zapstore App',
|
||||
30063: 'Zapstore Release',
|
||||
3063: 'Zapstore Asset',
|
||||
15128: 'Nsite',
|
||||
35128: 'Nsite',
|
||||
31124: 'Blobbi',
|
||||
|
||||
@@ -135,7 +135,7 @@ function MainLayoutInner() {
|
||||
being hidden. This depends on MobileTopBar having a transparent /
|
||||
semi-transparent background — a solid top bar would obscure the
|
||||
content underneath. Only active below the sidebar breakpoint. */}
|
||||
<div className={cn("relative flex-1 min-w-0 sidebar:border-l sidebar:border-r border-border bg-background/85", !hideTopBar && "-mt-mobile-bar", !noMaxWidth && "sidebar:max-w-[600px]", !noOverscroll && "pb-overscroll")}>
|
||||
<div className={cn("relative z-0 flex-1 min-w-0 sidebar:border-l sidebar:border-r border-border bg-background/85", !hideTopBar && "-mt-mobile-bar", !noMaxWidth && "sidebar:max-w-[600px]", !noOverscroll && "pb-overscroll")}>
|
||||
<Outlet />
|
||||
|
||||
{/* Desktop FAB — sticky within the feed column so it stays
|
||||
|
||||
@@ -539,11 +539,23 @@ export const NoteCard = memo(function NoteCard({
|
||||
) : isNsite ? (
|
||||
<NsiteCard event={event} />
|
||||
) : isZapstoreApp ? (
|
||||
<ZapstoreAppContent event={event} compact />
|
||||
<div className="mt-2 rounded-xl border border-border overflow-hidden transition-all duration-300 hover:shadow-md hover:border-primary/20">
|
||||
<div className="px-3.5 pb-3.5 pt-3">
|
||||
<ZapstoreAppContent event={event} compact />
|
||||
</div>
|
||||
</div>
|
||||
) : isZapstoreRelease ? (
|
||||
<ZapstoreReleaseContent event={event} compact />
|
||||
<div className="mt-2 rounded-xl border border-border overflow-hidden transition-all duration-300 hover:shadow-md hover:border-primary/20">
|
||||
<div className="px-3.5 pb-3.5 pt-3">
|
||||
<ZapstoreReleaseContent event={event} compact />
|
||||
</div>
|
||||
</div>
|
||||
) : isZapstoreAsset ? (
|
||||
<ZapstoreAssetContent event={event} compact />
|
||||
<div className="mt-2 rounded-xl border border-border overflow-hidden transition-all duration-300 hover:shadow-md hover:border-primary/20">
|
||||
<div className="px-3.5 pb-3.5 pt-3">
|
||||
<ZapstoreAssetContent event={event} compact />
|
||||
</div>
|
||||
</div>
|
||||
) : isAppHandler ? (
|
||||
<AppHandlerContent event={event} compact />
|
||||
) : isEncryptedDM ? (
|
||||
@@ -2014,15 +2026,15 @@ const KIND_HEADER_MAP: Record<number, KindHeaderConfig> = {
|
||||
},
|
||||
32267: {
|
||||
icon: Package,
|
||||
action: "published an app",
|
||||
action: "published a Zapstore app",
|
||||
},
|
||||
30063: {
|
||||
icon: Package,
|
||||
action: "published a release",
|
||||
action: "published a Zapstore release",
|
||||
},
|
||||
3063: {
|
||||
icon: Package,
|
||||
action: "published an asset",
|
||||
action: "published a Zapstore asset",
|
||||
},
|
||||
31990: {
|
||||
icon: Package,
|
||||
|
||||
@@ -255,7 +255,7 @@ export function ZapstoreAppContent({ event, compact }: ZapstoreAppContentProps)
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="mt-2 space-y-2.5">
|
||||
<div className="space-y-2.5">
|
||||
{/* Header: icon + name + summary */}
|
||||
<div className="flex items-start gap-3">
|
||||
{icon ? (
|
||||
@@ -326,7 +326,7 @@ export function ZapstoreAppContent({ event, compact }: ZapstoreAppContentProps)
|
||||
|
||||
// Full detail view
|
||||
return (
|
||||
<div className="mt-3 space-y-4">
|
||||
<div className="space-y-4">
|
||||
{/* Header: icon + name + summary */}
|
||||
<div className="flex items-start gap-4">
|
||||
{icon ? (
|
||||
|
||||
@@ -308,7 +308,7 @@ export function ZapstoreReleaseContent({ event, compact }: ZapstoreReleaseConten
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="mt-2 space-y-2.5">
|
||||
<div className="space-y-2.5">
|
||||
{/* Header: icon + app name + version */}
|
||||
<div className="flex items-start gap-3">
|
||||
{appIcon ? (
|
||||
@@ -367,7 +367,7 @@ export function ZapstoreReleaseContent({ event, compact }: ZapstoreReleaseConten
|
||||
|
||||
// Full detail view
|
||||
return (
|
||||
<div className="mt-3 space-y-4">
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-4">
|
||||
{appIcon ? (
|
||||
@@ -488,7 +488,7 @@ export function ZapstoreReleaseContent({ event, compact }: ZapstoreReleaseConten
|
||||
/** Skeleton loading state for ZapstoreReleaseContent. */
|
||||
export function ZapstoreReleaseSkeleton() {
|
||||
return (
|
||||
<div className="mt-3 space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<Skeleton className="size-14 rounded-2xl shrink-0" />
|
||||
<div className="flex-1 space-y-2">
|
||||
@@ -554,7 +554,7 @@ export function ZapstoreAssetContent({ event, compact }: ZapstoreAssetContentPro
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-10 rounded-xl bg-primary/10 flex items-center justify-center shrink-0">
|
||||
<PlatformIcon mime={mime} className="size-5 text-primary" />
|
||||
@@ -581,7 +581,7 @@ export function ZapstoreAssetContent({ event, compact }: ZapstoreAssetContentPro
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 space-y-4">
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="size-14 rounded-2xl bg-primary/10 flex items-center justify-center shrink-0">
|
||||
@@ -694,7 +694,7 @@ function MetaRow({ label, value }: { label: string; value: React.ReactNode }) {
|
||||
/** Skeleton for ZapstoreAssetContent. */
|
||||
export function ZapstoreAssetSkeleton() {
|
||||
return (
|
||||
<div className="mt-3 space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<Skeleton className="size-14 rounded-2xl shrink-0" />
|
||||
<div className="flex-1 space-y-2">
|
||||
|
||||
@@ -546,10 +546,10 @@ const KIND_SPECIFIC_LABELS: Record<number, string> = {
|
||||
35128: 'nsite',
|
||||
30008: 'profile badges',
|
||||
30817: 'repository issue',
|
||||
32267: 'app',
|
||||
32267: 'Zapstore app',
|
||||
31990: 'app',
|
||||
30063: 'release',
|
||||
3063: 'asset',
|
||||
30063: 'Zapstore release',
|
||||
3063: 'Zapstore asset',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -78,15 +78,15 @@ const NOTIFICATION_KIND_NOUNS: Record<number, string> = {
|
||||
30030: 'emoji pack',
|
||||
30054: 'podcast episode',
|
||||
30055: 'podcast trailer',
|
||||
3063: 'asset',
|
||||
30063: 'release',
|
||||
3063: 'Zapstore asset',
|
||||
30063: 'Zapstore release',
|
||||
30311: 'stream',
|
||||
30315: 'status',
|
||||
30617: 'repository',
|
||||
30817: 'custom NIP',
|
||||
31922: 'calendar event',
|
||||
31923: 'calendar event',
|
||||
32267: 'app',
|
||||
32267: 'Zapstore app',
|
||||
34139: 'playlist',
|
||||
34236: 'divine',
|
||||
34550: 'community',
|
||||
|
||||
@@ -136,9 +136,9 @@ function shellTitleForKind(kind?: number): string {
|
||||
if (kind === BADGE_DEFINITION_KIND) return "Badge Details";
|
||||
if (kind === BADGE_PROFILE_KIND_NEW || kind === BADGE_PROFILE_KIND_LEGACY) return "Badge Collection";
|
||||
if (kind === BOOK_REVIEW_KIND) return "Book Review";
|
||||
if (kind === 32267) return "App Details";
|
||||
if (kind === 30063) return "Release";
|
||||
if (kind === 3063) return "Asset";
|
||||
if (kind === 32267) return "Zapstore App";
|
||||
if (kind === 30063) return "Zapstore Release";
|
||||
if (kind === 3063) return "Zapstore Asset";
|
||||
if (kind === 31990) return "App";
|
||||
if (kind === 15128 || kind === 35128) return "Nsite";
|
||||
if (kind === VANISH_KIND) return "Request to Vanish";
|
||||
@@ -1950,6 +1950,15 @@ function PostDetailContent({ event }: { event: NostrEvent }) {
|
||||
{isAppHandler && (
|
||||
<EventActionHeader pubkey={event.pubkey} icon={Package} action="published an app" />
|
||||
)}
|
||||
{isZapstoreApp && (
|
||||
<EventActionHeader pubkey={event.pubkey} icon={Package} action="published a Zapstore app" />
|
||||
)}
|
||||
{isZapstoreRelease && (
|
||||
<EventActionHeader pubkey={event.pubkey} icon={Package} action="published a Zapstore release" />
|
||||
)}
|
||||
{isZapstoreAsset && (
|
||||
<EventActionHeader pubkey={event.pubkey} icon={Package} action="published a Zapstore asset" />
|
||||
)}
|
||||
{isNsite && (
|
||||
<EventActionHeader pubkey={event.pubkey} icon={Rocket} action="deployed an" noun="nsite" nounRoute="/development" />
|
||||
)}
|
||||
@@ -2064,15 +2073,21 @@ function PostDetailContent({ event }: { event: NostrEvent }) {
|
||||
<NsiteCard event={event} />
|
||||
</div>
|
||||
) : isZapstoreApp ? (
|
||||
<ZapstoreAppContent event={event} />
|
||||
<div className="mt-3 rounded-xl border border-border overflow-hidden px-4 pt-4 pb-4">
|
||||
<ZapstoreAppContent event={event} />
|
||||
</div>
|
||||
) : isZapstoreRelease ? (
|
||||
<Suspense fallback={<ZapstoreReleaseSkeleton />}>
|
||||
<ZapstoreReleaseContent event={event} />
|
||||
</Suspense>
|
||||
<div className="mt-3 rounded-xl border border-border overflow-hidden px-4 pt-4 pb-4">
|
||||
<Suspense fallback={<ZapstoreReleaseSkeleton />}>
|
||||
<ZapstoreReleaseContent event={event} />
|
||||
</Suspense>
|
||||
</div>
|
||||
) : isZapstoreAsset ? (
|
||||
<Suspense fallback={<ZapstoreAssetSkeleton />}>
|
||||
<ZapstoreAssetContent event={event} />
|
||||
</Suspense>
|
||||
<div className="mt-3 rounded-xl border border-border overflow-hidden px-4 pt-4 pb-4">
|
||||
<Suspense fallback={<ZapstoreAssetSkeleton />}>
|
||||
<ZapstoreAssetContent event={event} />
|
||||
</Suspense>
|
||||
</div>
|
||||
) : isAppHandler ? (
|
||||
<AppHandlerContent event={event} />
|
||||
) : isEncryptedDM ? (
|
||||
|
||||
Reference in New Issue
Block a user