catalog permalink

This commit is contained in:
2026-03-23 16:34:10 +01:00
parent 4ecc117f10
commit ec1f2c7b8f
3 changed files with 266 additions and 208 deletions
+36 -206
View File
@@ -27,144 +27,44 @@ const softwares = json.softwares;
</div>
<div class="grid grid-cols-1 gap-8 software-list">
{softwares.map(({ software, releases, latestRelease }) => {
const stableReleases = releases.filter((r: any) => !r.version.startsWith('dev-'));
const devReleases = releases.filter((r: any) => r.version.startsWith('dev-'));
{softwares.map(({ software, releases }) => {
const latestStable = releases
.filter((r: any) => !r.version.startsWith('dev-'))
.sort((a: any, b: any) => new Date(b.UpdatedAt).getTime() - new Date(a.UpdatedAt).getTime())[0];
return (
<div class="software-card" data-status={software.status}>
<div class="software-card group" data-status={software.status}>
<div class="software-card-content">
<div class="flex items-center gap-3 mb-2">
<h2 class="software-title mb-0">{software.title}</h2>
<span class={`status-badge status-${software.status}`}>
{software.status}
</span>
</div>
<p class="software-desc">{software.desc}</p>
<div class="software-meta">
<div>Author: {software.author}</div>
<div>Platform: {software.platform}</div>
</div>
{latestRelease && (
<div class="latest-release-actions">
{latestRelease.htmlFolderPath ? (
<a href={BASE + latestRelease.htmlFolderPath} target="_blank" class="btn-play-sm">▶ Play</a>
) : (
<span class="btn-disabled-sm">▶ Play</span>
)}
{latestRelease.cartridgePath ? (
<a href={BASE + latestRelease.cartridgePath} target="_blank" class="btn-download-sm">Download</a>
) : (
<span class="btn-disabled-sm">Download</span>
)}
{latestRelease.sourcePath ? (
<a href={BASE + latestRelease.sourcePath} target="_blank" class="btn-source-sm">Source</a>
) : (
<span class="btn-disabled-sm">Source</span>
)}
{latestRelease.docsFolderPath ? (
<a href={BASE + latestRelease.docsFolderPath} target="_blank" class="btn-docs-sm">Docs</a>
) : (
<span class="btn-disabled-sm">Docs</span>
)}
</div>
)}
<details class="mt-4">
<summary class="releases-summary">
Releases ({releases.length})
</summary>
<div class="mt-4">
{/* Desktop Table View */}
<div class="desktop-releases">
<table class="releases-table">
<thead class="releases-thead">
<tr>
<th scope="col" class="releases-th">Version</th>
<th scope="col" class="releases-th">Created</th>
<th scope="col" class="releases-th">Updated</th>
<th scope="col" class="releases-th !text-right">Actions</th>
</tr>
</thead>
<tbody class="releases-tbody">
{stableReleases.map((release: any) => (
<tr>
<td class="releases-td font-medium text-gray-900">{release.version}</td>
<td class="releases-td text-gray-500">{formatDateTime(release.CreatedAt)}</td>
<td class="releases-td text-gray-500">{formatDateTime(release.UpdatedAt)}</td>
<td class="releases-td text-right">
{release.htmlFolderPath ? (<a href={BASE + release.htmlFolderPath} target="_blank" class="action-link-purple">▶ Play</a>) : (<span class="action-link-disabled">▶ Play</span>)}
{release.cartridgePath ? (<a href={BASE + release.cartridgePath} target="_blank" class="action-link-blue">Download</a>) : (<span class="action-link-disabled">Download</span>)}
{release.sourcePath ? (<a href={BASE + release.sourcePath} target="_blank" class="action-link-green">Source</a>) : (<span class="action-link-disabled">Source</span>)}
{release.docsFolderPath ? (<a href={BASE + release.docsFolderPath} target="_blank" class="action-link-yellow">Docs</a>) : (<span class="action-link-disabled">Docs</span>)}
</td>
</tr>
))}
{devReleases.length > 0 && (
<tr>
<td colspan="4" class="releases-td text-xs font-bold text-gray-400 uppercase tracking-wider pt-3 pb-1">Development Releases</td>
</tr>
)}
{devReleases.map((release: any) => (
<tr class="bg-yellow-50/50">
<td class="releases-td font-medium text-yellow-800">{release.version}</td>
<td class="releases-td text-gray-500">{formatDateTime(release.CreatedAt)}</td>
<td class="releases-td text-gray-500">{formatDateTime(release.UpdatedAt)}</td>
<td class="releases-td text-right">
{release.htmlFolderPath ? (<a href={BASE + release.htmlFolderPath} target="_blank" class="action-link-purple">▶ Play</a>) : (<span class="action-link-disabled">▶ Play</span>)}
{release.cartridgePath ? (<a href={BASE + release.cartridgePath} target="_blank" class="action-link-blue">Download</a>) : (<span class="action-link-disabled">Download</span>)}
{release.sourcePath ? (<a href={BASE + release.sourcePath} target="_blank" class="action-link-green">Source</a>) : (<span class="action-link-disabled">Source</span>)}
{release.docsFolderPath ? (<a href={BASE + release.docsFolderPath} target="_blank" class="action-link-yellow">Docs</a>) : (<span class="action-link-disabled">Docs</span>)}
</td>
</tr>
))}
</tbody>
</table>
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div class="flex-grow">
<div class="flex items-center gap-3 mb-2">
<a href={`/catalog/${software.name}`} class="hover:text-purple-600 transition-colors">
<h2 class="software-title mb-0">{software.title}</h2>
</a>
<span class={`status-badge status-${software.status}`}>
{software.status}
</span>
</div>
<p class="software-desc max-w-2xl">{software.desc}</p>
<div class="software-meta">
<span>Author: <span class="text-gray-900 font-medium">{software.author}</span></span>
<span class="mx-2 text-gray-300">•</span>
<span>Platform: <span class="text-gray-900 font-medium">{software.platform}</span></span>
</div>
</div>
{/* Mobile Stacked View */}
<div class="mobile-releases">
{stableReleases.map((release: any) => (
<div class="release-mobile-card">
<div class="release-mobile-header">
<span class="release-mobile-version">{release.version}</span>
<span class="release-mobile-date">Created: {formatDateTime(release.CreatedAt)}</span>
<span class="release-mobile-date">Updated: {formatDateTime(release.UpdatedAt)}</span>
</div>
<div class="release-mobile-grid">
{release.htmlFolderPath ? (<a href={BASE + release.htmlFolderPath} target="_blank" class="mobile-btn-play">▶ Play</a>) : (<span class="mobile-btn-disabled">▶ Play</span>)}
{release.cartridgePath ? (<a href={BASE + release.cartridgePath} target="_blank" class="mobile-btn-download">Download</a>) : (<span class="mobile-btn-disabled">Download</span>)}
{release.sourcePath ? (<a href={BASE + release.sourcePath} target="_blank" class="mobile-btn-source">Source</a>) : (<span class="mobile-btn-disabled">Source</span>)}
{release.docsFolderPath ? (<a href={BASE + release.docsFolderPath} target="_blank" class="mobile-btn-docs">Docs</a>) : (<span class="mobile-btn-disabled">Docs</span>)}
</div>
</div>
))}
{devReleases.length > 0 && (
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider pt-3 pb-1">Development Releases</div>
<div class="flex flex-wrap gap-2 items-center">
{latestStable?.htmlFolderPath && (
<a href={BASE + latestStable.htmlFolderPath} target="_blank" class="btn-play-sm">▶ Play</a>
)}
{devReleases.map((release: any) => (
<div class="release-mobile-card border-yellow-200 bg-yellow-50">
<div class="release-mobile-header">
<span class="release-mobile-version text-yellow-800">{release.version}</span>
<span class="release-mobile-date">Created: {formatDateTime(release.CreatedAt)}</span>
<span class="release-mobile-date">Updated: {formatDateTime(release.UpdatedAt)}</span>
</div>
<div class="release-mobile-grid">
{release.htmlFolderPath ? (<a href={BASE + release.htmlFolderPath} target="_blank" class="mobile-btn-play">▶ Play</a>) : (<span class="mobile-btn-disabled">▶ Play</span>)}
{release.cartridgePath ? (<a href={BASE + release.cartridgePath} target="_blank" class="mobile-btn-download">Download</a>) : (<span class="mobile-btn-disabled">Download</span>)}
{release.sourcePath ? (<a href={BASE + release.sourcePath} target="_blank" class="mobile-btn-source">Source</a>) : (<span class="mobile-btn-disabled">Source</span>)}
{release.docsFolderPath ? (<a href={BASE + release.docsFolderPath} target="_blank" class="mobile-btn-docs">Docs</a>) : (<span class="mobile-btn-disabled">Docs</span>)}
</div>
</div>
))}
<a href={`/catalog/${software.name}`} class="btn-info-sm">
More Info
</a>
</div>
</div>
</details>
</div>
</div>
</div>
)
})}
)})}
</div>
</main>
</Layout>
@@ -216,89 +116,19 @@ const softwares = json.softwares;
@apply p-6;
}
.software-title {
@apply text-2xl font-semibold mb-2;
@apply text-2xl font-bold mb-0;
}
.software-desc {
@apply text-gray-600 mb-4;
@apply text-gray-600 mb-3;
}
.software-meta {
@apply text-sm text-gray-500 mb-4;
}
.latest-release-actions {
@apply flex flex-row flex-wrap gap-1 mb-4 sm:absolute sm:top-4 sm:right-4 sm:flex-col sm:mb-0 z-10;
@apply text-sm text-gray-500;
}
/* Small Action Buttons */
.btn-base-sm {
@apply inline-block font-bold py-1 px-2 rounded-lg text-sm transition-colors;
@apply inline-flex items-center justify-center font-bold py-2.5 px-5 rounded-xl text-sm transition-all active:scale-95;
}
.btn-play-sm { @apply btn-base-sm bg-purple-600 hover:bg-purple-700 text-white; }
.btn-download-sm { @apply btn-base-sm bg-blue-600 hover:bg-blue-700 text-white; }
.btn-source-sm { @apply btn-base-sm bg-green-600 hover:bg-green-700 text-white; }
.btn-docs-sm { @apply btn-base-sm bg-yellow-600 hover:bg-yellow-700 text-white; }
.btn-disabled-sm { @apply btn-base-sm bg-gray-200 text-gray-400 cursor-not-allowed; }
.releases-summary {
@apply cursor-pointer font-semibold text-purple-600 hover:text-purple-800;
}
/* Desktop Table Styles */
.desktop-releases {
@apply hidden sm:block overflow-x-auto;
}
.releases-table {
@apply min-w-full divide-y divide-gray-200;
}
.releases-thead {
@apply bg-gray-50;
}
.releases-th {
@apply px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider;
}
.releases-tbody {
@apply bg-white divide-y divide-gray-200;
}
.releases-td {
@apply px-3 py-2 whitespace-nowrap text-sm;
}
/* Action Links */
.action-link-base {
@apply hover:underline mr-2 last:mr-0;
}
.action-link-purple { @apply action-link-base text-purple-600 hover:text-purple-900; }
.action-link-blue { @apply action-link-base text-blue-600 hover:text-blue-900; }
.action-link-green { @apply action-link-base text-green-600 hover:text-green-900; }
.action-link-yellow { @apply action-link-base text-yellow-600 hover:text-yellow-900; }
.action-link-disabled { @apply text-gray-400 cursor-not-allowed mr-2 last:mr-0; }
/* Mobile Release Styles */
.mobile-releases {
@apply sm:hidden space-y-4;
}
.release-mobile-card {
@apply border border-gray-100 rounded-lg p-4 bg-gray-50 shadow-sm;
}
.release-mobile-header {
@apply flex flex-col mb-3 border-b border-gray-200 pb-2;
}
.release-mobile-version {
@apply font-bold text-gray-900 text-lg break-words;
}
.release-mobile-date {
@apply text-xs text-gray-500 mt-1;
}
.release-mobile-grid {
@apply grid grid-cols-2 gap-2;
}
/* Mobile Buttons */
.mobile-btn-base {
@apply flex items-center justify-center py-2 px-3 rounded text-sm font-bold shadow-sm;
}
.mobile-btn-play { @apply mobile-btn-base bg-purple-600 text-white; }
.mobile-btn-download { @apply mobile-btn-base bg-blue-600 text-white; }
.mobile-btn-source { @apply mobile-btn-base bg-green-600 text-white; }
.mobile-btn-docs { @apply mobile-btn-base bg-yellow-600 text-white; }
.mobile-btn-disabled { @apply mobile-btn-base bg-gray-200 text-gray-400 cursor-not-allowed; }
.btn-play-sm { @apply btn-base-sm bg-purple-600 hover:bg-purple-700 text-white shadow-lg shadow-purple-600/20; }
.btn-info-sm { @apply btn-base-sm bg-gray-100 hover:bg-gray-200 text-gray-700; }
</style>
@@ -0,0 +1,228 @@
---
import Layout from '../../layouts/Layout.astro';
import { formatDateTime } from '../../utils/dateFormat';
export async function getStaticPaths() {
const BASE = "https://games.teletype.hu";
const API = BASE + "/api/software";
const res = await fetch(API);
const json = await res.json();
const softwares = json.softwares;
return softwares.map((item: any) => ({
params: { name: item.software.name },
props: { project: item },
}));
}
const { project } = Astro.props;
const { software, releases } = project;
const BASE = "https://games.teletype.hu";
const stableReleases = releases
.filter((r: any) => !r.version.startsWith('dev-'))
.sort((a: any, b: any) => new Date(b.UpdatedAt).getTime() - new Date(a.UpdatedAt).getTime());
const devReleases = releases
.filter((r: any) => r.version.startsWith('dev-'))
.sort((a: any, b: any) => new Date(b.UpdatedAt).getTime() - new Date(a.UpdatedAt).getTime());
const latestStable = stableReleases[0];
---
<Layout>
<div class="bg-gray-50 min-h-screen pb-20">
<header class="hero-section-gradient from-purple-700 to-indigo-800 py-12 md:py-20">
<div class="hero-container">
<nav class="mb-8">
<a href="/catalog" class="text-purple-200 hover:text-white transition-colors flex items-center gap-2 font-medium">
← Back to Catalog
</a>
</nav>
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6">
<div class="flex-grow">
<div class="flex items-center gap-3 mb-4">
<span class={`status-badge status-${software.status} px-3 py-1`}>
{software.status}
</span>
<span class="text-purple-300 font-mono text-sm tracking-widest uppercase">{software.platform}</span>
</div>
<h1 class="text-4xl md:text-6xl font-black text-white mb-4 leading-tight">{software.title}</h1>
<p class="text-xl text-purple-100 max-w-3xl leading-relaxed">{software.desc}</p>
</div>
</div>
</div>
</header>
<main class="main-container -mt-10">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Sidebar Info -->
<div class="lg:col-span-1 space-y-6">
<div class="bg-white rounded-3xl shadow-xl p-8 border border-gray-100">
<h2 class="text-xl font-bold text-gray-900 mb-6 flex items-center gap-2">
<span class="text-purple-600"></span> Project Info
</h2>
<dl class="space-y-4">
<div>
<dt class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">Author</dt>
<dd class="text-gray-900 font-medium">{software.author}</dd>
</div>
<div>
<dt class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">Platform</dt>
<dd class="text-gray-900 font-medium">{software.platform}</dd>
</div>
{software.license && (
<div>
<dt class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">License</dt>
<dd class="text-gray-900 font-medium">{software.license}</dd>
</div>
)}
{software.site && (
<div>
<dt class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">Source Code</dt>
<dd>
<a href={software.site} target="_blank" class="text-purple-600 hover:text-purple-800 font-bold break-all">
Visit Repository ↗
</a>
</dd>
</div>
)}
</dl>
</div>
</div>
<!-- Main Content Area -->
<div class="lg:col-span-2 space-y-8">
<!-- Latest Release (Prominent Section) -->
{latestStable && (
<div class="bg-gradient-to-br from-indigo-600 to-purple-700 rounded-3xl shadow-xl p-8 text-white">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
<div>
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-0.5 bg-white/20 text-white text-[10px] font-black uppercase rounded tracking-widest">Latest Stable</span>
</div>
<h2 class="text-4xl font-black mb-1">{latestStable.version}</h2>
<p class="text-indigo-100 text-sm">Released: {formatDateTime(latestStable.UpdatedAt)}</p>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-2 xl:grid-cols-4 gap-2 w-full md:w-auto">
{latestStable.htmlFolderPath && (
<a href={BASE + latestStable.htmlFolderPath} target="_blank" class="flex items-center justify-center py-3 px-4 bg-white text-indigo-900 font-bold rounded-xl transition-all hover:bg-indigo-50 shadow-lg shadow-black/10 text-sm whitespace-nowrap">
▶ Play Now
</a>
)}
{latestStable.cartridgePath && (
<a href={BASE + latestStable.cartridgePath} target="_blank" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">
💾 Download
</a>
)}
{latestStable.sourcePath && (
<a href={BASE + latestStable.sourcePath} target="_blank" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">
📄 Source
</a>
)}
{latestStable.docsFolderPath && (
<a href={BASE + latestStable.docsFolderPath} target="_blank" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">
📖 Docs
</a>
)}
</div>
</div>
</div>
)}
<!-- Releases List Box -->
<div class="bg-white rounded-3xl shadow-xl overflow-hidden border border-gray-100">
<div class="p-8 border-b border-gray-100">
<h2 class="text-2xl font-bold text-gray-900">All Releases</h2>
</div>
<div class="p-0">
{/* Desktop Table */}
<div class="hidden md:block">
<table class="w-full text-left">
<thead class="bg-gray-50 text-gray-500 text-xs font-bold uppercase tracking-widest">
<tr>
<th class="px-8 py-4">Version</th>
<th class="px-8 py-4">Release Date</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{stableReleases.map((release: any) => (
<tr class="hover:bg-gray-50/50 transition-colors">
<td class="px-8 py-4">
<div class="font-bold text-gray-900 text-lg mb-2">{release.version}</div>
<div class="flex flex-wrap gap-4">
{release.htmlFolderPath && <a href={BASE + release.htmlFolderPath} target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm flex items-center gap-1">▶ Play</a>}
{release.cartridgePath && <a href={BASE + release.cartridgePath} target="_blank" class="text-blue-600 hover:text-blue-900 font-bold text-sm flex items-center gap-1">💾 Download</a>}
{release.sourcePath && <a href={BASE + release.sourcePath} target="_blank" class="text-green-600 hover:text-green-900 font-bold text-sm flex items-center gap-1">📄 Source</a>}
{release.docsFolderPath && <a href={BASE + release.docsFolderPath} target="_blank" class="text-yellow-600 hover:text-yellow-900 font-bold text-sm flex items-center gap-1">📖 Docs</a>}
</div>
</td>
<td class="px-8 py-4 text-gray-500 align-top pt-5">{formatDateTime(release.UpdatedAt)}</td>
</tr>
))}
{devReleases.length > 0 && (
<tr class="bg-yellow-50/30">
<td colspan="2" class="px-8 py-3 text-[10px] font-black text-yellow-700 uppercase tracking-tighter">Development Versions</td>
</tr>
)}
{devReleases.map((release: any) => (
<tr class="bg-yellow-50/10 hover:bg-yellow-50/30 transition-colors">
<td class="px-8 py-4">
<div class="font-bold text-yellow-800 text-lg mb-2">{release.version}</div>
<div class="flex flex-wrap gap-4">
{release.htmlFolderPath && <a href={BASE + release.htmlFolderPath} target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm flex items-center gap-1">▶ Play</a>}
{release.cartridgePath && <a href={BASE + release.cartridgePath} target="_blank" class="text-blue-600 hover:text-blue-900 font-bold text-sm flex items-center gap-1">💾 Download</a>}
</div>
</td>
<td class="px-8 py-4 text-gray-500 align-top pt-5">{formatDateTime(release.UpdatedAt)}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Mobile View */}
<div class="md:hidden divide-y divide-gray-100">
{releases.map((release: any) => (
<div class={`p-6 ${release.version.startsWith('dev-') ? 'bg-yellow-50/20' : ''}`}>
<div class="flex justify-between items-start mb-4">
<div>
<div class="font-bold text-gray-900 text-lg">{release.version}</div>
<div class="text-gray-500 text-xs mt-1">{formatDateTime(release.UpdatedAt)}</div>
</div>
{release.version.startsWith('dev-') && (
<span class="px-2 py-0.5 bg-yellow-100 text-yellow-800 text-[10px] font-black uppercase rounded">Dev</span>
)}
</div>
<div class="grid grid-cols-2 gap-2">
{release.htmlFolderPath && <a href={BASE + release.htmlFolderPath} target="_blank" class="flex items-center justify-center py-2 bg-purple-100 text-purple-700 font-bold rounded-lg text-sm">Play</a>}
{release.cartridgePath && <a href={BASE + release.cartridgePath} target="_blank" class="flex items-center justify-center py-2 bg-blue-100 text-blue-700 font-bold rounded-lg text-sm">Download</a>}
{release.sourcePath && <a href={BASE + release.sourcePath} target="_blank" class="flex items-center justify-center py-2 bg-green-100 text-green-700 font-bold rounded-lg text-sm">Source</a>}
{release.docsFolderPath && <a href={BASE + release.docsFolderPath} target="_blank" class="flex items-center justify-center py-2 bg-yellow-100 text-yellow-700 font-bold rounded-lg text-sm">Docs</a>}
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</Layout>
<style>
.status-badge {
@apply rounded text-[10px] font-black uppercase tracking-wider;
}
.status-released { @apply bg-green-500 text-white shadow-lg shadow-green-500/20; }
.status-demo { @apply bg-blue-500 text-white shadow-lg shadow-blue-500/20; }
.status-development { @apply bg-yellow-500 text-white shadow-lg shadow-yellow-500/20; }
.status-archived { @apply bg-gray-500 text-white shadow-lg shadow-gray-500/20; }
</style>
+2 -2
View File
@@ -156,8 +156,8 @@ const YOUTUBE_CHANNEL_ID = import.meta.env.YOUTUBE_CHANNEL_ID;
▶ Play in Browser
</a>
)}
<a href="/catalog" class="featured-btn-secondary">
View in Catalog
<a href={`/catalog/${highlightedSoftware.software.name}`} class="featured-btn-secondary">
View Project Details
</a>
</div>
</div>