import Link from "next/link";
import type { ReactNode } from "react";
import { deleteReleaseAction } from "@/app/actions/releases";
import { CopyButton } from "@/components/clone-button";
import { ConfirmButton } from "@/components/confirm-button";
import { ShaChip } from "@/components/file-table";
import { CodeIcon, DownloadIcon, PackageIcon, PencilIcon, TagIcon } from "@/components/icons";
import { Markdown } from "@/components/markdown";
import { liftRowClass, plateHeadingClass } from "@/components/repo-rows";
import { Avatar, Box, ButtonLink, Label, RefChip } from "@/components/ui";
import { encodePath, formatBytes, formatDate, timeAgo } from "@/lib/format";
import { releaseTitle, type Release } from "@/lib/releases";
import type { Repo } from "@/lib/repo";
export const releaseHref = (repo: Repo, tag: string) => `${repo.base}/releases/tag/${encodePath(tag)}`;
export const assetHref = (repo: Repo, tag: string, name: string) => `${repo.base}/releases/download/${encodePath(tag)}/${encodeURIComponent(name)}`;
function Time({ ms, prefix }: { ms: number; prefix?: string }) {
return (
<span className="type-meta text-ink3">
{prefix && `${prefix} `}
<time dateTime={new Date(ms).toISOString()} title={formatDate(ms)} className="type-readout">
{timeAgo(ms)}
</time>
</span>
);
}
function AssetRow({ href, name, icon, children }: { href: string; name: string; icon: ReactNode; children?: ReactNode }) {
return (
<li className={`${liftRowClass} flex min-h-11 flex-wrap items-center gap-x-3 gap-y-1 px-3 py-1.5`}>
<span className="icon-tile size-7 shrink-0 rounded-row">{icon}</span>
<a href={href} download className="min-w-0 truncate font-mono text-[12px] text-ink decoration-ink3 underline-offset-4 hover:underline">
{name}
</a>
{children}
</li>
);
}
/**
* A release on its plate: what it is (title, state, tag, commit, who and when), its notes, and its
* files with the source code at the tag.
*/
export function ReleaseCard({ repo, release, sha, latest, linkTitle = true }: { repo: Repo; release: Release; sha: string | null; latest: boolean; linkTitle?: boolean }) {
const title = releaseTitle(release);
const tagPath = `refs/tags/${encodePath(release.tag)}`;
return (
<Box className="overflow-hidden">
<div className="flex flex-col gap-2.5 px-6 pt-5 pb-4">
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
<h2 className="min-w-0 text-[19px] leading-7 font-semibold tracking-[-0.02em] text-ink">
{linkTitle ? (
<Link href={releaseHref(repo, release.tag)} className="decoration-ink3 underline-offset-4 hover:underline">
{title}
</Link>
) : (
title
)}
</h2>
{release.draft && <Label tone="attention">Draft</Label>}
{release.prerelease && <Label tone="attention">Pre-release</Label>}
{latest && <Label tone="success">Latest</Label>}
{repo.canWrite && (
<div className="ml-auto flex items-center gap-1">
<ButtonLink href={`${repo.base}/releases/edit/${encodePath(release.tag)}`} variant="invisible" icon={<PencilIcon width={14} height={14} />}>
Edit
</ButtonLink>
<ConfirmButton
action={deleteReleaseAction.bind(null, repo.owner, repo.name, release.id)}
message={`Delete the release ${title}? ${release.assets.length === 1 ? "Its file is" : "Its files are"} deleted with it${sha ? `; the tag ${release.tag} stays` : ""}.`}
variant="default"
>
Delete
</ConfirmButton>
</div>
)}
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">
<span className="inline-flex items-center gap-1.5">
<TagIcon width={13} height={13} className="text-ink3" />
<RefChip>{release.tag}</RefChip>
</span>
{sha ? (
<ShaChip sha={sha} href={`${repo.base}/commit/${sha}`} />
) : (
<span className="type-meta text-ink3">
{release.draft ? <>Tag is created from <RefChip>{release.target}</RefChip> when published</> : "Tag no longer exists"}
</span>
)}
{release.author && (
<Link href={`/${release.author}`} className="inline-flex items-center gap-1.5 type-meta font-medium text-ink2 hover:text-ink">
<Avatar name={release.author} size={18} /> {release.author}
</Link>
)}
{release.publishedAt ? <Time ms={release.publishedAt} prefix="released" /> : <Time ms={release.createdAt} prefix="drafted" />}
</div>
</div>
{release.body.trim() && (
<div className="rule-t px-6 py-5">
<Markdown context={sha ? { base: repo.base, gitRef: release.tag, dir: "" } : undefined}>{release.body}</Markdown>
</div>
)}
<div className="rule-t p-1.5">
<h3 className={plateHeadingClass}>
Assets <span className="type-readout text-ink3 [text-shadow:none]">{release.assets.length + (sha ? 2 : 0)}</span>
</h3>
<ul>
{release.assets.map((a) => (
<AssetRow key={a.id} href={assetHref(repo, release.tag, a.name)} name={a.name} icon={<PackageIcon width={14} height={14} />}>
<span className="ml-auto flex shrink-0 items-center gap-0.5 max-md:hidden">
<code className="type-readout text-ink3" title={`SHA-256 ${a.sha256}`}>
sha256:{a.sha256.slice(0, 12)}
</code>
<CopyButton text={a.sha256} label="Copy SHA-256 checksum" />
</span>
<span className="w-20 shrink-0 text-right type-readout text-ink2 max-md:ml-auto">{formatBytes(a.size)}</span>
<span className="flex w-14 shrink-0 items-center justify-end gap-1 type-readout text-ink3" title={`${a.downloads} downloads`}>
<DownloadIcon width={12} height={12} /> {a.downloads}
</span>
<span className="w-24 shrink-0 text-right max-sm:hidden">
<Time ms={a.updatedAt} />
</span>
</AssetRow>
))}
{sha && (
<>
<AssetRow href={`${repo.base}/archive/${tagPath}.zip`} name="Source code (zip)" icon={<CodeIcon width={14} height={14} />} />
<AssetRow href={`${repo.base}/archive/${tagPath}.tar.gz`} name="Source code (tar.gz)" icon={<CodeIcon width={14} height={14} />} />
</>
)}
</ul>
{release.assets.length === 0 && !sha && <p className="px-3 pb-3 type-meta text-ink3">No files yet.</p>}
</div>
</Box>
);
}