refactor: Update Zapstore app set renderers with improved UX

- ZapstoreAppSetRenderer: Show ALL apps with compact spacing (gap-0.5) like relay lists, removed 5-app limit
- ZapstoreAppSetDetailRenderer: Replace raw platform tags with normalized platform icons using detectPlatforms()
- Both renderers now provide cleaner, more consistent UI following Grimoire patterns
This commit is contained in:
Claude
2026-01-11 19:47:21 +00:00
parent 92b6d476b4
commit 8719c56f3c
5 changed files with 100 additions and 79 deletions

View File

@@ -6,11 +6,9 @@ import {
import {
getAppName,
getAppSummary,
getAppIcon,
detectPlatforms,
} from "@/lib/zapstore-helpers";
import {
Package,
Globe,
Smartphone,
TabletSmartphone,
@@ -79,52 +77,34 @@ function PlatformIcon({ platform }: { platform: Platform }) {
export function ZapstoreAppRenderer({ event }: BaseEventProps) {
const appName = getAppName(event);
const summary = getAppSummary(event);
const iconUrl = getAppIcon(event);
const platforms = detectPlatforms(event);
return (
<BaseEventContainer event={event}>
<div className="flex gap-3">
{/* App Icon */}
{iconUrl ? (
<img
src={iconUrl}
alt={appName}
className="size-12 rounded-lg object-cover flex-shrink-0"
loading="lazy"
/>
) : (
<div className="size-12 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
<Package className="size-6 text-muted-foreground" />
</div>
<div className="flex flex-col gap-2">
{/* App Name */}
<ClickableEventTitle
event={event}
className="text-base font-semibold text-foreground"
>
{appName}
</ClickableEventTitle>
{/* Summary */}
{summary && (
<p className="text-sm text-muted-foreground line-clamp-2">
{summary}
</p>
)}
{/* App Info */}
<div className="flex flex-col gap-2 flex-1 min-w-0">
{/* App Name */}
<ClickableEventTitle
event={event}
className="text-base font-semibold text-foreground"
>
{appName}
</ClickableEventTitle>
{/* Summary */}
{summary && (
<p className="text-sm text-muted-foreground line-clamp-2">
{summary}
</p>
)}
{/* Platform Icons */}
{platforms.length > 0 && (
<div className="flex items-center gap-2">
{platforms.map((platform) => (
<PlatformIcon key={platform} platform={platform} />
))}
</div>
)}
</div>
{/* Platform Icons */}
{platforms.length > 0 && (
<div className="flex items-center gap-2">
{platforms.map((platform) => (
<PlatformIcon key={platform} platform={platform} />
))}
</div>
)}
</div>
</BaseEventContainer>
);

View File

@@ -5,19 +5,79 @@ import {
getAppName,
getAppSummary,
getAppIcon,
getAppPlatforms,
detectPlatforms,
getCurationSetIdentifier,
} from "@/lib/zapstore-helpers";
import { Badge } from "@/components/ui/badge";
import type { Platform } from "@/lib/zapstore-helpers";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { useGrimoire } from "@/core/state";
import { UserName } from "../UserName";
import { Package } from "lucide-react";
import {
Package,
Globe,
Smartphone,
TabletSmartphone,
Monitor,
Laptop,
} from "lucide-react";
interface ZapstoreAppSetDetailRendererProps {
event: NostrEvent;
}
/**
* Platform icon component with label
*/
function PlatformIcon({ platform }: { platform: Platform }) {
const iconClass = "size-4 text-muted-foreground";
const getPlatformLabel = () => {
switch (platform) {
case "android":
return "Android";
case "ios":
return "iOS";
case "web":
return "Web";
case "macos":
return "macOS";
case "windows":
return "Windows";
case "linux":
return "Linux";
default:
return platform;
}
};
const getIcon = () => {
switch (platform) {
case "android":
return <TabletSmartphone className={iconClass} />;
case "ios":
return <Smartphone className={iconClass} />;
case "web":
return <Globe className={iconClass} />;
case "macos":
return <Laptop className={iconClass} />;
case "windows":
case "linux":
return <Monitor className={iconClass} />;
default:
return null;
}
};
return (
<div className="flex items-center gap-1.5">
{getIcon()}
<span className="text-xs text-muted-foreground">
{getPlatformLabel()}
</span>
</div>
);
}
/**
* Expanded app card showing full app details
*/
@@ -45,7 +105,7 @@ function AppCard({
const appName = getAppName(appEvent);
const summary = getAppSummary(appEvent);
const iconUrl = getAppIcon(appEvent);
const platforms = getAppPlatforms(appEvent);
const platforms = detectPlatforms(appEvent);
const handleClick = () => {
addWindow("open", { pointer: address });
@@ -84,23 +144,12 @@ function AppCard({
</p>
)}
{/* Platforms */}
{/* Platform Icons */}
{platforms.length > 0 && (
<div className="flex flex-wrap gap-2">
{platforms.slice(0, 6).map((platform) => (
<Badge
key={platform}
variant="secondary"
className="text-[10px] px-2 py-0.5"
>
{platform}
</Badge>
<div className="flex items-center gap-2">
{platforms.map((platform) => (
<PlatformIcon key={platform} platform={platform} />
))}
{platforms.length > 6 && (
<Badge variant="outline" className="text-[10px] px-2 py-0">
+{platforms.length - 6} more
</Badge>
)}
</div>
)}
</div>

View File

@@ -45,17 +45,12 @@ function AppItem({
/**
* Renderer for Kind 30267 - Zapstore App Curation Set
* Displays collection name and list of apps
* Displays collection name and list of all apps with compact layout
*/
export function ZapstoreAppSetRenderer({ event }: BaseEventProps) {
const setName = getCurationSetName(event);
const apps = getAppReferences(event);
// Show max 5 apps in feed view
const MAX_APPS_IN_FEED = 5;
const displayApps = apps.slice(0, MAX_APPS_IN_FEED);
const remainingCount = apps.length - displayApps.length;
return (
<BaseEventContainer event={event}>
<div className="flex flex-col gap-2">
@@ -72,17 +67,12 @@ export function ZapstoreAppSetRenderer({ event }: BaseEventProps) {
{apps.length} {apps.length === 1 ? "app" : "apps"}
</p>
{/* App List */}
{displayApps.length > 0 && (
<div className="flex flex-col gap-1.5 pl-4 border-l-2 border-muted">
{displayApps.map((ref, idx) => (
{/* App List - Show all apps with compact spacing like relay lists */}
{apps.length > 0 && (
<div className="flex flex-col gap-0.5">
{apps.map((ref, idx) => (
<AppItem key={idx} address={ref.address} />
))}
{remainingCount > 0 && (
<span className="text-xs text-muted-foreground">
+{remainingCount} more app{remainingCount > 1 ? "s" : ""}
</span>
)}
</div>
)}
</div>

View File

@@ -37,8 +37,10 @@ export function ZapstoreReleaseDetailRenderer({
// Fetch related events
const appEvent = useNostrEvent(appPointer || undefined);
// Load file event with release event as context for better relay selection
const fileEvent = useNostrEvent(
fileEventId ? { id: fileEventId } : undefined,
event, // Pass release event as context to use author's relays
);
const appName = appEvent ? getAppName(appEvent) : appPointer?.identifier;

View File

@@ -58,14 +58,14 @@ export function ZapstoreReleaseRenderer({ event }: BaseEventProps) {
{/* Links */}
<div className="flex items-center gap-3 flex-wrap text-sm">
{/* App Link */}
{/* App Link - show app name with icon */}
{appName && (
<button
onClick={handleAppClick}
className="flex items-center gap-1.5 text-primary hover:underline"
>
<Package className="size-3" />
<span>View App</span>
<span>{appName}</span>
</button>
)}
@@ -76,7 +76,7 @@ export function ZapstoreReleaseRenderer({ event }: BaseEventProps) {
className="flex items-center gap-1.5 text-primary hover:underline"
>
<FileDown className="size-3" />
<span>Download File</span>
<span>Download</span>
</button>
)}
</div>