mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 23:47:12 +02:00
feat: Add releases section to app detail view
- Query for all releases (kind 30063) that reference the app - Display releases sorted by version (newest first) - Each release shows version badge and download link - Clicking release opens full release detail view - Clicking download opens file metadata view
This commit is contained in:
@@ -8,11 +8,18 @@ import {
|
||||
getAppRepository,
|
||||
getAppLicense,
|
||||
getAppIdentifier,
|
||||
getReleaseVersion,
|
||||
getReleaseFileEventId,
|
||||
} from "@/lib/zapstore-helpers";
|
||||
import type { Platform } from "@/lib/zapstore-helpers";
|
||||
import { UserName } from "../UserName";
|
||||
import { ExternalLink } from "@/components/ExternalLink";
|
||||
import { MediaEmbed } from "../MediaEmbed";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { use$ } from "applesauce-react/hooks";
|
||||
import eventStore from "@/services/event-store";
|
||||
import { useMemo } from "react";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import {
|
||||
Package,
|
||||
Globe,
|
||||
@@ -20,12 +27,68 @@ import {
|
||||
TabletSmartphone,
|
||||
Monitor,
|
||||
Laptop,
|
||||
FileDown,
|
||||
} from "lucide-react";
|
||||
|
||||
interface ZapstoreAppDetailRendererProps {
|
||||
event: NostrEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release item component showing version and download link
|
||||
*/
|
||||
function ReleaseItem({ release }: { release: NostrEvent }) {
|
||||
const { addWindow } = useGrimoire();
|
||||
const version = getReleaseVersion(release);
|
||||
const fileEventId = getReleaseFileEventId(release);
|
||||
|
||||
const handleClick = () => {
|
||||
addWindow("open", {
|
||||
pointer: {
|
||||
kind: release.kind,
|
||||
pubkey: release.pubkey,
|
||||
identifier: release.tags.find((t) => t[0] === "d")?.[1] || "",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (fileEventId) {
|
||||
addWindow("open", { pointer: { id: fileEventId } });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-3 bg-muted/20 rounded-lg hover:bg-muted/30 transition-colors">
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="flex items-center gap-2 hover:underline cursor-crosshair"
|
||||
>
|
||||
<Package className="size-4 text-muted-foreground" />
|
||||
<span className="font-medium">
|
||||
{version ? `Version ${version}` : "Release"}
|
||||
</span>
|
||||
{version && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
v{version}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{fileEventId && (
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="flex items-center gap-1.5 text-primary hover:underline text-sm"
|
||||
>
|
||||
<FileDown className="size-4" />
|
||||
<span>Download</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform icon and label component
|
||||
*/
|
||||
@@ -79,7 +142,7 @@ function PlatformItem({ platform }: { platform: Platform }) {
|
||||
|
||||
/**
|
||||
* Detail renderer for Kind 32267 - App
|
||||
* Shows comprehensive app information including screenshots and platforms
|
||||
* Shows comprehensive app information including screenshots, platforms, and releases
|
||||
*/
|
||||
export function ZapstoreAppDetailRenderer({
|
||||
event,
|
||||
@@ -93,6 +156,36 @@ export function ZapstoreAppDetailRenderer({
|
||||
const license = getAppLicense(event);
|
||||
const identifier = getAppIdentifier(event);
|
||||
|
||||
// Query for releases that reference this app
|
||||
const releasesFilter = useMemo(() => {
|
||||
if (!identifier) {
|
||||
// Return a filter that matches nothing when no identifier
|
||||
return { kinds: [30063], ids: [] };
|
||||
}
|
||||
return {
|
||||
kinds: [30063],
|
||||
"#a": [`32267:${event.pubkey}:${identifier}`],
|
||||
};
|
||||
}, [event.pubkey, identifier]);
|
||||
|
||||
const releases = use$(
|
||||
() => eventStore.timeline(releasesFilter),
|
||||
[releasesFilter],
|
||||
);
|
||||
|
||||
// Sort releases by version (newest first) or created_at
|
||||
const sortedReleases = useMemo(() => {
|
||||
const releasesList = releases || [];
|
||||
return [...releasesList].sort((a, b) => {
|
||||
const versionA = getReleaseVersion(a);
|
||||
const versionB = getReleaseVersion(b);
|
||||
if (versionA && versionB) {
|
||||
return versionB.localeCompare(versionA, undefined, { numeric: true });
|
||||
}
|
||||
return b.created_at - a.created_at;
|
||||
});
|
||||
}, [releases]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 max-w-4xl mx-auto">
|
||||
{/* Header Section */}
|
||||
@@ -169,6 +262,20 @@ export function ZapstoreAppDetailRenderer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Releases Section */}
|
||||
{sortedReleases.length > 0 && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-xl font-semibold">
|
||||
Releases ({sortedReleases.length})
|
||||
</h2>
|
||||
<div className="flex flex-col gap-2">
|
||||
{sortedReleases.map((release) => (
|
||||
<ReleaseItem key={release.id} release={release} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Screenshots Section */}
|
||||
{images.length > 0 && (
|
||||
<div className="flex flex-col gap-3">
|
||||
|
||||
Reference in New Issue
Block a user