- {CODE} tokens in description/actions/flavor render as inline symbol
icons (RichText), including inside cost pips
- custom frames gain an optional background texture (upload/replace/
remove via /api/custom-frames/[id]/texture); renders behind panels
- custom games can be shared to the community (is_public): toggle in the
game space, public listing at /community/games, read-only game view,
/api/public/games endpoints (no auth, public rows only)
- /designer/print: multi-card print sheets on US Letter at 300dpi
(63x88mm cards, 3x3 or 2x2, dashed cut guides, full-sheet PNG export)
- migration 1787711511000
107 lines
3.9 KiB
JavaScript
107 lines
3.9 KiB
JavaScript
import { useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/router';
|
|
import Layout from '../../../components/Layout';
|
|
import { useAuth } from '../../../lib/use-auth';
|
|
|
|
export default function CommunityGames() {
|
|
const router = useRouter();
|
|
const { user } = useAuth();
|
|
|
|
const [games, setGames] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
|
|
useEffect(() => {
|
|
const loadGames = async () => {
|
|
try {
|
|
const response = await fetch('/api/public/games');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setGames(data.games);
|
|
} else {
|
|
setError('Failed to load community games.');
|
|
}
|
|
} catch {
|
|
setError('Failed to load community games.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadGames();
|
|
}, []);
|
|
|
|
return (
|
|
<Layout user={user}>
|
|
<div className="p-4 sm:p-6 max-w-[1500px] mx-auto space-y-6">
|
|
<div className="pt-2">
|
|
<h1 className="text-2xl sm:text-3xl font-bold mb-1" style={{ color: 'var(--text-primary)' }}>
|
|
Community Games
|
|
</h1>
|
|
<p className="text-base" style={{ color: 'var(--text-secondary)' }}>
|
|
Custom game systems shared by the community — browse their cards.
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="glass-panel rounded-xl px-4 py-3 text-sm" style={{ color: '#f87171' }} role="alert">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="flex justify-center py-16">
|
|
<div className="animate-spin rounded-full h-16 w-16 border-b-2" style={{ borderColor: 'var(--accent-ember)' }} />
|
|
</div>
|
|
) : games.length === 0 ? (
|
|
<div className="text-center py-16">
|
|
<h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
No shared games yet
|
|
</h3>
|
|
<p style={{ color: 'var(--text-secondary)' }}>
|
|
{user
|
|
? 'Share one of your custom games from its game space to see it here.'
|
|
: 'Sign in and share a custom game to see it here.'}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
|
{games.map((game) => (
|
|
<div
|
|
key={game.id}
|
|
className="glass-panel rounded-2xl p-5 cursor-pointer transition-all duration-200 hover:shadow-lg"
|
|
onClick={() => router.push(`/community/games/${game.id}`)}
|
|
role="link"
|
|
tabIndex={0}
|
|
onKeyDown={(e) => e.key === 'Enter' && router.push(`/community/games/${game.id}`)}
|
|
>
|
|
<h3 className="text-lg font-bold mb-1 truncate" style={{ color: 'var(--text-primary)' }}>
|
|
{game.name}
|
|
</h3>
|
|
<p className="text-sm mb-3 line-clamp-2" style={{ color: 'var(--text-secondary)' }}>
|
|
{game.description || 'No description'}
|
|
</p>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-xs font-semibold" style={{ color: 'var(--accent-ember)' }}>
|
|
{game.card_count} card{game.card_count === 1 ? '' : 's'} →
|
|
</span>
|
|
<span className="text-xs truncate ml-2" style={{ color: 'var(--text-secondary)' }}>
|
|
by {game.author}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{user && (
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
Managing your own games? Head to{' '}
|
|
<Link href="/games" style={{ color: 'var(--accent-ember)' }}>My Games</Link>.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|