Initial commit: Dostalgia — DOS game hub
- Go backend (single binary) with embedded Svelte frontend - JSON-per-game storage (no database) - .jsdos bundle creation on upload - Dark phosphor-green theme (#33FF33) - js-dos v8 emulator integration - Sockdrive support for games >80MB - IGDB placeholder for future artwork scraping - Docker deployment ready
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
<script>
|
||||
import { listGames, uploadGame } from "../lib/api.js";
|
||||
import GameCard from "../lib/GameCard.svelte";
|
||||
|
||||
let games = $state([]);
|
||||
let loading = $state(true);
|
||||
let search = $state("");
|
||||
let uploading = $state(false);
|
||||
let uploadError = $state("");
|
||||
let fileInput;
|
||||
|
||||
async function loadGames() {
|
||||
loading = true;
|
||||
try {
|
||||
const data = await listGames(search ? { search } : {});
|
||||
games = data.games || [];
|
||||
} catch (e) {
|
||||
console.error("Failed to load games:", e);
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function handleUpload(e) {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
uploading = true;
|
||||
uploadError = "";
|
||||
try {
|
||||
await uploadGame(file);
|
||||
await loadGames();
|
||||
fileInput.value = "";
|
||||
} catch (err) {
|
||||
uploadError = err.message;
|
||||
}
|
||||
uploading = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
loadGames();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="library">
|
||||
<div class="toolbar">
|
||||
<h1>Library</h1>
|
||||
<div class="toolbar-actions">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search games..."
|
||||
bind:value={search}
|
||||
oninput={() => loadGames()}
|
||||
/>
|
||||
<label class="btn btn-primary upload-label">
|
||||
{uploading ? "Uploading..." : "+ Upload Game"}
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
type="file"
|
||||
accept=".zip"
|
||||
class="hidden-input"
|
||||
onchange={handleUpload}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if uploadError}
|
||||
<div class="error">{uploadError}</div>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="loading">Scanning library...</div>
|
||||
{:else if games.length === 0}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">💾</div>
|
||||
<h2>No games yet</h2>
|
||||
<p>Upload a DOS game ZIP to get started</p>
|
||||
<label class="btn btn-primary" style="margin-top: 16px;">
|
||||
Upload your first game
|
||||
<input type="file" accept=".zip" class="hidden-input" onchange={handleUpload} />
|
||||
</label>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid">
|
||||
{#each games as game (game.id)}
|
||||
<GameCard {game} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="count">{games.length} game{games.length !== 1 ? "s" : ""}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.library {
|
||||
padding: 32px 0;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
.toolbar h1 {
|
||||
color: var(--phosphor);
|
||||
font-family: var(--font-mono);
|
||||
animation: phosphor-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.toolbar-actions input {
|
||||
min-width: 200px;
|
||||
}
|
||||
.hidden-input {
|
||||
display: none;
|
||||
}
|
||||
.upload-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.error {
|
||||
background: #331111;
|
||||
border: 1px solid #cc3333;
|
||||
color: #ff6666;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-dim);
|
||||
font-style: italic;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.empty-state h2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.empty-state p {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.count {
|
||||
margin-top: 24px;
|
||||
text-align: center;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user