Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions site/scripts/generate-pack-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ interface RegistryEntry {
tags?: string[];
quality?: "gold" | "silver" | "flagged" | "unreviewed";
added?: string;
updated?: string;
}

interface PackData {
Expand All @@ -159,6 +160,7 @@ interface PackData {
sourceRepo?: string;
sourcePath?: string;
dateAdded?: string;
dateUpdated?: string;
}

// ── Config ──────────────────────────────────────────────────────────────────
Expand All @@ -179,7 +181,7 @@ const REGISTRY_INDEX_URL =
// ── Helpers ─────────────────────────────────────────────────────────────────
// audioBase should be the URL of the directory containing sounds/
// e.g. "https://raw.../og-packs/v1.0.0/peon" or "https://raw.../mypack/v1.0.0"
function processManifest(manifest: Manifest, packName: string, audioBase: string, trustTier: string = "community", registryTags?: string[], sourceRepo?: string, sourcePath?: string, quality?: "gold" | "silver" | "flagged" | "unreviewed", dateAdded?: string): PackData {
function processManifest(manifest: Manifest, packName: string, audioBase: string, trustTier: string = "community", registryTags?: string[], sourceRepo?: string, sourcePath?: string, quality?: "gold" | "silver" | "flagged" | "unreviewed", dateAdded?: string, dateUpdated?: string): PackData {
const categories: PackData["categories"] = [];
const previewSounds: PackData["previewSounds"] = [];
let soundCount = 0;
Expand Down Expand Up @@ -224,6 +226,7 @@ function processManifest(manifest: Manifest, packName: string, audioBase: string
sourceRepo,
sourcePath,
dateAdded,
dateUpdated,
};
}

Expand Down Expand Up @@ -305,7 +308,7 @@ async function generateFromRemote(): Promise<PackData[]> {
? `${rawBase}/${entry.source_path}`
: rawBase;

return processManifest(manifest, packName, audioBase, entry.trust_tier || "community", entry.tags, entry.source_repo, entry.source_path || undefined, entry.quality, entry.added);
return processManifest(manifest, packName, audioBase, entry.trust_tier || "community", entry.tags, entry.source_repo, entry.source_path || undefined, entry.quality, entry.added, entry.updated);
})
);

Expand Down
19 changes: 19 additions & 0 deletions site/src/components/ui/AudioPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ export function AudioPlayer({
label,
id,
compact,
iconOnly,
}: {
url: string;
label: string;
id: string;
compact?: boolean;
iconOnly?: boolean;
}) {
const { play, stop, currentId } = useAudio();
const isPlaying = currentId === id;
Expand All @@ -79,6 +81,23 @@ export function AudioPlayer({
}
};

if (iconOnly) {
return (
<button
onClick={handleClick}
aria-label={`${isPlaying ? "Stop" : "Play"}: ${label}`}
title={label}
className={`flex-shrink-0 flex items-center justify-center w-7 h-7 rounded-full transition-colors text-xs ${
isPlaying
? "bg-gold text-black shadow-[0_0_8px_rgba(255,171,1,0.3)]"
: "bg-surface-border text-text-muted hover:bg-gold/20 hover:text-gold"
}`}
>
{isPlaying ? "■" : "▶"}
</button>
);
}

return (
<button
onClick={handleClick}
Expand Down
144 changes: 82 additions & 62 deletions site/src/components/ui/PackCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";
import type { PackMeta } from "@/lib/types";
import { AudioPlayer } from "./AudioPlayer";

Expand All @@ -22,88 +21,109 @@ function TierBadge({ tier }: { tier: string }) {
);
}

function formatDate(iso: string): { short: string; tooltip: string } {
const date = new Date(iso);
const now = new Date();
const month = date.toLocaleString("en-US", { month: "short" });
const day = date.getDate();
const year = date.getFullYear();
const short = year === now.getFullYear() ? `${month} ${day}` : `${month} ${day}, ${year}`;
return { short, tooltip: `${month} ${day}, ${year}` };
}

function DateDisplay({ pack }: { pack: PackMeta }) {
const raw = pack.dateUpdated || pack.dateAdded;
if (!raw) return null;
const isUpdated = !!pack.dateUpdated;
const { short, tooltip } = formatDate(raw);
return (
<span title={`${isUpdated ? "Updated" : "Added"} ${tooltip}`}>
{short}
</span>
);
}

export function PackCard({ pack }: { pack: PackMeta }) {
const router = useRouter();
const preview = pack.previewSounds[0];

return (
<Link
href={`/packs/${pack.name}`}
className="group flex flex-col rounded-lg border border-surface-border bg-surface-card p-4 transition-all duration-200 hover:border-gold/50 hover:bg-surface-card/80"
className="group flex flex-col rounded-lg border border-surface-border bg-surface-card transition-all duration-200 hover:border-gold/50 hover:bg-surface-card/80"
>
{/* Name */}
<h3 className="font-display text-lg text-text-primary group-hover:text-gold transition-colors truncate mb-1">
{pack.displayName}
</h3>
{/* ── Title Bar ── */}
<div className="px-4 pt-2 pb-2 border-b border-surface-border">
<h3 className="font-display text-lg text-text-primary group-hover:text-gold transition-colors truncate" title={pack.displayName}>
{pack.displayName}
</h3>
</div>

{/* Description */}
{pack.description && (
<p className="text-xs text-text-muted mb-2 line-clamp-2">
{pack.description}
{/* ── Content Zone ── */}
<div className="flex-1 px-4 py-3 flex flex-col gap-2">
{/* Description — fixed height to keep alignment across cards */}
<p className="text-xs text-text-muted line-clamp-2 min-h-[2.5rem]">
{pack.description || "\u00A0"}
</p>
)}

{/* Tags */}
{pack.tags && pack.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mb-2">
{pack.tags.slice(0, 5).map((tag) => (
<span
key={tag}
className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-surface-bg border border-surface-border text-text-subtle"
>
{tag}
</span>
))}
{/* Badges */}
<div className="flex gap-1.5">
<TierBadge tier={pack.trustTier} />
<span className="font-mono text-[10px] px-2 py-0.5 rounded-full uppercase border border-amber-700/50 text-amber-500">
{pack.languageLabel}
</span>
</div>
)}

{/* Meta line */}
<div className="font-mono text-[11px] text-text-dim mb-2">
{pack.author.name || pack.author.github}
<span className="mx-1 opacity-50">&middot;</span>
{pack.totalSoundCount} sounds
<span className="mx-1 opacity-50">&middot;</span>
v{pack.version}
</div>
{/* Tags — single line, overflow hidden */}
{pack.tags && pack.tags.length > 0 && (
<div className="flex flex-wrap gap-1 overflow-hidden max-h-[1.5rem]">
{pack.tags.map((tag) => (
<span
key={tag}
className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-surface-bg border border-surface-border text-text-subtle"
>
{tag}
</span>
))}
</div>
)}

{/* Badges */}
<div className="flex gap-1.5 mb-3">
<TierBadge tier={pack.trustTier} />
<span className="font-mono text-[10px] px-2 py-0.5 rounded-full uppercase border border-amber-700/50 text-amber-500">
{pack.languageLabel}
</span>
{/* Author */}
{(pack.author.name || pack.author.github) && (
<p className="font-mono text-[11px] text-text-dim">
{pack.author.name || pack.author.github}
</p>
)}
</div>

{/* Audio preview */}
{preview && (
<div className="mt-auto">
{/* ── Status Bar ── */}
<div className="px-4 py-2 border-t border-surface-border flex items-center justify-between">
{/* Play button */}
{preview ? (
<AudioPlayer
url={preview.audioUrl}
label={preview.label}
id={`card-${pack.name}`}
compact
iconOnly
/>
</div>
)}
) : (
<div className="w-7 h-7" />
)}

{pack.sourceRepo && (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
const hash = encodeURIComponent(
pack.sourcePath
? pack.sourceRepo + "/" + pack.sourcePath
: pack.sourceRepo!
);
router.push(`/preview#${hash}`);
}}
className="block mt-2 text-left text-[11px] font-medium text-gold hover:text-gold/80 transition-colors"
>
preview &rarr;
</button>
)}
{/* Stats */}
<div className="font-mono text-xs text-text-dim flex items-center gap-1.5">
<span title={`${pack.totalSoundCount} sounds`}>
♫ {pack.totalSoundCount}
</span>
<span className="opacity-40">&middot;</span>
<span>v{pack.version}</span>
{(pack.dateUpdated || pack.dateAdded) && (
<>
<span className="opacity-40">&middot;</span>
<DateDisplay pack={pack} />
</>
)}
</div>
</div>
</Link>
);
}
1 change: 1 addition & 0 deletions site/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface PackMeta {
sourceRepo?: string;
sourcePath?: string;
dateAdded?: string;
dateUpdated?: string;
}

export interface PacksData {
Expand Down
Loading