Initial Code

This commit is contained in:
2025-10-24 01:32:42 -04:00
parent 62f568794a
commit 844253ed13
19 changed files with 744 additions and 126 deletions

145
.gitignore vendored Normal file → Executable file
View File

@@ -1,132 +1,25 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
node_modules
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# OS
.DS_Store
Thumbs.db
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
# Env
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
.env.*
!.env.example
!.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
bun.lock

40
package.json Executable file
View File

@@ -0,0 +1,40 @@
{
"name": "humandex-web",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"start": "bun run build && node server/server.js",
"dev": "vite dev --host",
"build": "vite build"
},
"devDependencies": {
"@skeletonlabs/skeleton": "^3.2.2",
"@skeletonlabs/skeleton-svelte": "^1.5.3",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/kit": "^2.47.3",
"@sveltejs/vite-plugin-svelte": "^5.1.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.16",
"mdsvex": "^0.12.6",
"svelte": "^5.41.2",
"svelte-check": "^4.3.3",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"vite": "^6.4.1"
},
"dependencies": {
"@auth/sveltekit": "^1.11.0",
"@iconify/svelte": "^5.0.2",
"@sveltejs/adapter-node": "^5.4.0",
"axios": "^1.12.2",
"bufferutil": "^4.0.9",
"cors": "^2.8.5",
"discord.js": "^14.23.2",
"dotenv": "^16.6.1",
"express": "^5.1.0",
"mongoose": "^8.19.2",
"ms": "^2.1.3"
}
}

28
server/server.js Executable file
View File

@@ -0,0 +1,28 @@
// @ts-nocheck
import { handler } from '../build/handler.js';
import dotenv from 'dotenv';
import http from 'http';
import express from 'express';
import cors from 'cors';
dotenv.config();
const app = express();
const server = http.Server(app);
app.use(cors());
app.use(handler);
server.listen(process.env.PORT, () => {
console.log(`listening on port http://localhost:${process.env.PORT}`);
});
process.on('unhandledRejection', (reason, promise) => {
console.log('Unhandled Rejection at:', reason.stack || reason);
});
process.on('uncaughtException', (error) => {
console.log('Uncaught Exception thrown');
console.error(error);
});

9
src/app.css Executable file
View File

@@ -0,0 +1,9 @@
@import 'tailwindcss';
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';
@import '@skeletonlabs/skeleton';
@import '@skeletonlabs/skeleton/optional/presets';
@import '@skeletonlabs/skeleton/themes/cerberus';
@source '../node_modules/@skeletonlabs/skeleton-svelte/dist';

13
src/app.d.ts vendored Executable file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

12
src/app.html Executable file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" data-theme="cerberus">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,105 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let id: string | number;
export let name: string = 'Unknown';
export let defaultImage: string | null = null; // optional URL
export let className: string = '';
export let editable: boolean = false;
const dispatch = createEventDispatcher();
let imageSrc: string | null = defaultImage;
let fileInput: HTMLInputElement | null = null;
// check for stored custom image in localStorage on mount
import { onMount } from 'svelte';
let displayName: string = name;
let storedNameLoaded = false;
onMount(() => {
try {
const key = `dex-${id}`;
const stored = localStorage.getItem(key);
if (stored) {
const parsed = JSON.parse(stored) as { imageSrc?: string; name?: string };
if (parsed?.imageSrc) imageSrc = parsed.imageSrc;
if (parsed?.name) { displayName = parsed.name; storedNameLoaded = true; }
}
} catch (e) {
// ignore
}
});
// keep displayName in sync with prop name unless a stored override exists
$: if (!storedNameLoaded) displayName = name;
function onClick() {
fileInput?.click();
}
function onFileChange(e: Event) {
const target = e.target as HTMLInputElement;
const file = target.files && target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
imageSrc = String(reader.result);
dispatch('change', { id, imageSrc });
};
reader.readAsDataURL(file);
}
function reset() {
imageSrc = defaultImage;
dispatch('change', { id, imageSrc });
if (fileInput) fileInput.value = '';
}
</script>
<div class="flex flex-col items-center justify-start p-2 {className}">
<div class="w-full bg-surface-900 rounded overflow-hidden shadow-sm">
{#if editable}
<div role="button" tabindex="0" class="w-full h-44 bg-surface-900 flex items-center justify-center relative cursor-pointer" on:click={onClick} on:keydown={(e) => { if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') { e.preventDefault(); onClick(); } }} title="Click to replace image">
{#if imageSrc}
<img src={imageSrc} alt={displayName} class="object-cover w-full h-full" />
{:else}
<div class="flex flex-col items-center justify-center text-center p-4 text-surface-400">
<div class="text-2xl font-semibold mb-1">{displayName}</div>
<div class="text-xs">Click to add your photo</div>
</div>
{/if}
<div class="absolute top-2 right-2 flex gap-2">
<button type="button" class="px-2 py-1 bg-black/40 text-white text-xs rounded" on:click|stopPropagation={reset}>Reset</button>
</div>
</div>
{:else}
<div class="w-full h-44 bg-surface-900 flex items-center justify-center relative">
{#if imageSrc}
<img src={imageSrc} alt={displayName} class="object-cover w-full h-full" />
{:else}
<div class="flex flex-col items-center justify-center text-center p-4 text-surface-400">
<div class="text-2xl font-semibold mb-1">{displayName}</div>
<div class="text-xs">No image</div>
</div>
{/if}
<div class="absolute top-2 right-2 flex gap-2">
<a class="px-2 py-1 bg-black/40 text-white text-xs rounded" href={`/dex/${id}`} on:click|stopPropagation>Open</a>
</div>
</div>
{/if}
<div class="p-2 text-xs text-surface-400">
<div class="font-medium">{name}</div>
<div class="text-[11px] mt-1">#{id}</div>
</div>
</div>
<input bind:this={fileInput} type="file" accept="image/*" class="hidden" on:change={onFileChange} />
</div>
<style>
/* keep images pixelated-looking when scaled (useful for sprites) */
img { image-rendering: pixelated; image-rendering: crisp-edges; }
</style>

View File

@@ -0,0 +1,110 @@
<script>
import Icon from "@iconify/svelte";
</script>
<footer class="bg-surface-900 text-gray-300 py-8 mt-auto">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- About Section -->
<div class="flex flex-col items-center text-center space-y-3">
<img src="/favicon.png" alt="GMMC Logo" class="h-16"/>
<h3 class="text-xl font-bold text-white">GMMC</h3>
<p class="max-w-xs text-sm text-gray-400">
The official Minecraft club at George Mason University. Join us for
building, events, and fun!
</p>
</div>
<!-- Quick Links (inline, separated by dots) -->
<div class="flex flex-col items-center text-center space-y-3">
<h3 class="text-lg font-semibold text-white">Quick Links</h3>
<ul class="flex flex-wrap items-center gap-2 text-sm">
<li class="inline-flex items-center">
<a href="/about" class="hover:text-green-400 transition-colors">About</a>
</li>
<li class="text-gray-500">&middot;</li>
<li class="inline-flex items-center">
<a href="/servers" class="hover:text-green-400 transition-colors">Servers</a>
</li>
<li class="text-gray-500">&middot;</li>
<li class="inline-flex items-center">
<a href="/whitelist" class="hover:text-green-400 transition-colors">Whitelist</a>
</li>
<li class="text-gray-500">&middot;</li>
<li class="inline-flex items-center">
<a href="/events" class="hover:text-green-400 transition-colors">Events</a>
</li>
</ul>
</div>
<!-- Contact & Social -->
<div class="flex flex-col items-center text-center space-y-3">
<h3 class="text-lg font-semibold text-white">Connect With Us</h3>
<div class="text-sm space-y-2">
<div class="flex items-center justify-center gap-4 mt-2">
<a
href="https://discord.gg/J9Ya3uQgJt"
class="text-gray-400 hover:text-blue-400 transition-colors p-2 rounded-full bg-surface-800/20"
aria-label="Discord"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon="ic:outline-discord" class="w-6 h-6" />
</a>
<a
href="https://mason360.gmu.edu/feeds?type=club&type_id=74218&tab=about"
class="text-gray-400 hover:text-blue-400 transition-colors p-2 rounded-full bg-surface-800/20"
aria-label="Discord"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon="teenyicons:360-outline" class="w-6 h-6" />
</a>
<a
href="https://mason360.gmu.edu/GMMC/club_signup"
class="text-gray-400 hover:text-blue-400 transition-colors p-2 rounded-full bg-surface-800/20"
aria-label="Discord"
target="_blank"
rel="noopener noreferrer"
>
<Icon icon="ant-design:link-outlined" class="w-6 h-6" />
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Full-width disclaimer (moved outside the centered container so it spans the footer) -->
<div class="w-full mt-8 pt-6 border-t border-surface-800 text-center text-sm text-gray-500 px-4">
<p>
University Public Disclaimer: The programs and services offered by
George Mason University are open to all who seek them. George Mason does
not discriminate on the basis of race, color, religion, ethnic national
origin (including shared ancestry and/or ethnic characteristics), sex,
disability, military status (including veteran status), sexual
orientation, gender identity, gender expression, age, marital status,
pregnancy status, genetic information, or any other characteristic
protected by law. After an initial review of its policies and practices,
the university affirms its commitment to meet all federal mandates as
articulated in federal law, as well as recent executive orders and
federal agency directives.University Public Disclaimer: The programs and
services offered by George Mason University are open to all who seek
them. George Mason does not discriminate on the basis of race, color,
religion, ethnic national origin (including shared ancestry and/or
ethnic characteristics), sex, disability, military status (including
veteran status), sexual orientation, gender identity, gender expression,
age, marital status, pregnancy status, genetic information, or any other
characteristic protected by law. After an initial review of its policies
and practices, the university affirms its commitment to meet all federal
mandates as articulated in federal law, as well as recent executive
orders and federal agency directives.
</p>
</div>
</footer>
<style>
footer {
box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1);
}
</style>

View File

@@ -0,0 +1,64 @@
<script>
import Icon from "@iconify/svelte";
let isOpen = false;
const toggleMenu = () => (isOpen = !isOpen);
</script>
<nav class="sticky top-0 z-50 shadow-xl bg-surface-900 text-black dark:text-white">
<div class="container mx-auto px-4">
<div class="grid grid-cols-[auto_1fr_auto] items-center gap-4 p-4">
<!-- Logo -->
<a class="flex" href="/">
<Icon icon="ic:twotone-catching-pokemon" class="w-8 h-8 text-red-400" />
<span class="text-xl font-bold ml-2 my-auto">HumanDex</span>
</a>
<!-- Mobile Menu Button -->
<!-- <button class="md:hidden justify-self-end" on:click={toggleMenu}>
{#if isOpen}
<Icon icon="eva:close-outline" class="w-6 h-6" />
{:else}
<Icon icon="eva:menu-outline" class="w-6 h-6" />
{/if}
</button> -->
<!-- Desktop Navigation -->
<!-- <div class="hidden md:flex items-center gap-4 justify-center font-semibold">
<a href="/servers" class="btn variant-ghost">Dex</a>
</div> -->
<!-- <div class="hidden md:flex justify-self-end gap-2">
<a href="https://trello.com/b/wjJqU9ws" class="btn preset-filled">
<Icon icon="mdi:trello" class="w-6 h-6" />
</a>
<a href="https://github.com/dead-projects-inc/pkit-cli" class="btn preset-filled">
<Icon icon="mdi:github" class="w-6 h-6" />
</a>
<a href="https://discord.gg/MHYCWXc83m" class="btn preset-filled">
<Icon icon="mdi:discord" class="w-6 h-6" />
</a>
</div> -->
</div>
<!-- Mobile Menu -->
{#if isOpen}
<div class="md:hidden p-4 space-y-3 font-semibold border-t border-surface-600">
<!-- <div class="flex flex-col space-y-3">
<a href="/" class="btn variant-ghost">Dex</a>
</div> -->
<!-- <div class="flex justify-center gap-4 pt-3 border-t border-surface-600">
<a href="https://trello.com/b/wjJqU9ws" class="btn preset-filled" title="Trello">
<Icon icon="mdi:trello" class="w-6 h-6" />
</a>
<a href="https://github.com/dead-projects-inc/pkit-cli" class="btn preset-filled" title="GitHub">
<Icon icon="mdi:github" class="w-6 h-6" />
</a>
<a href="https://discord.gg/MHYCWXc83m" class="btn preset-filled" title="Discord">
<Icon icon="mdi:discord" class="w-6 h-6" />
</a>
</div> -->
</div>
{/if}
</div>
</nav>

51
src/lib/ts/Userdb.ts Executable file
View File

@@ -0,0 +1,51 @@
import mongoose from 'mongoose';
const reqString = {
type: String,
required: true,
}
const userSchema = new mongoose.Schema({
id: reqString,
image: String,
});
export interface UserData {
id: string; // Discord ID
image?: string; // Base64 encoded image string
}
export default class Userdb {
model: mongoose.Model<any>;
upsert: { upsert: boolean; };
constructor() {
this.model = mongoose.model('users', userSchema);
this.upsert = { upsert: true };
}
async create(id: string, email: string, uuid: string): Promise<UserData|null> {
await this.model.findOneAndUpdate({ id: id }, { id: id }, this.upsert);
return await this.get(id);
}
async getAll(): Promise<UserData[]> {
return await this.model.find();
}
async get(Id: string): Promise<UserData|null> {
return await this.model.findOne({ id: Id });
}
async update(Id: string, data: any): Promise<UserData|null> {
await this.model.findOneAndUpdate({ id: Id }, data, this.upsert);
return await this.get(Id);
}
async delete(Id: string): Promise<UserData|null> {
return await this.model.findOneAndDelete({ id: Id });
}
}

20
src/lib/ts/db.ts Executable file
View File

@@ -0,0 +1,20 @@
import { SECRET_DB_URI } from "$env/static/private";
import mongoose from "mongoose";
import Playerdb from "./Userdb";
class DB {
dbs: any;
players: Playerdb;
constructor() {
this.players = new Playerdb();
this.connect();
}
connect() {
mongoose.set('strictQuery', true);
mongoose.connect(SECRET_DB_URI).then(() => { console.log("Connected to DataBase") });
}
}
export const db = new DB();

3
src/routes/+error.svelte Executable file
View File

@@ -0,0 +1,3 @@
<main class="container w-full mx-auto space-y-5 px-4 py-20">
<h1 class="text-4xl text-center font-semibold">404 or 500 or idk :(</h1>
</main>

26
src/routes/+layout.svelte Executable file
View File

@@ -0,0 +1,26 @@
<script lang="ts">
import '../app.css';
import NavBar from '$lib/components/NavBar.svelte';
import Footer from '$lib/components/Footer.svelte';
let { children } = $props();
</script>
<svelte:head>
<title>HumanDex</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
</svelte:head>
<main class="min-h-screen max-h-screen max-w-screen flex flex-col">
<NavBar />
<div class="flex flex-1">
{@render children()}
</div>
<Footer />
</main>

87
src/routes/+page.svelte Executable file
View File

@@ -0,0 +1,87 @@
<script lang="ts">
// using native <details> for accordion behavior to avoid component typing issues
import DexEntry from '$lib/components/DexEntry.svelte';
// Generations 1..8 (you can extend)
const generations = [
{ id: 1, name: 'Generation I' },
{ id: 2, name: 'Generation II' },
{ id: 3, name: 'Generation III' },
{ id: 4, name: 'Generation IV' },
{ id: 5, name: 'Generation V' },
{ id: 6, name: 'Generation VI' },
{ id: 7, name: 'Generation VII' },
{ id: 8, name: 'Generation VIII' },
];
// store loaded data per generation
type PokeTile = { id: number; name: string; image: string };
let genData: Record<number, { loading: boolean; loaded: boolean; pokemon: PokeTile[] }> = {} as any;
// which accordion items are open (Accordion uses string[] values)
let openValues: string[] = [];
function onTileChange(event: CustomEvent) {
const { id, imageSrc } = event.detail;
console.log('Tile changed', id, imageSrc?.slice?.(0, 80));
// TODO: Persist to server or local storage
}
async function loadGeneration(genId: number) {
if (genData[genId]?.loaded || genData[genId]?.loading) return;
genData[genId] = { loading: true, loaded: false, pokemon: [] };
try {
const res = await fetch(`https://pokeapi.co/api/v2/generation/${genId}`);
if (!res.ok) throw new Error(`Failed to load generation ${genId}`);
const data = await res.json();
// `pokemon_species` is an array of { name, url } where url contains the species id
const species = data.pokemon_species as Array<{ name: string; url: string }>;
// map to id, name, image (official artwork by id). Extract id from url (last number)
const mapped: PokeTile[] = species.map(s => {
const parts = s.url.split('/').filter(Boolean);
const id = Number(parts[parts.length - 1]);
const image = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${id}.png`;
return { id, name: s.name, image };
}).sort((a, b) => a.id - b.id);
genData[genId] = { loading: false, loaded: true, pokemon: mapped };
} catch (err) {
console.error(err);
genData[genId] = { loading: false, loaded: false, pokemon: [] };
}
}
// when open values change, load newly opened gens
$: if (openValues && openValues.length) {
for (const val of openValues) {
const id = Number(String(val).replace(/[^0-9]/g, ''));
if (id) loadGeneration(id);
}
}
</script>
<div class="w-full max-w-screen min-h-[92dvh] flex flex-col items-center justify-start p-6">
<h1 class="text-3xl font-bold mb-6">HumanDex — Pokémon Pokedex for Humans</h1>
<div class="w-full max-w-6xl">
{#each generations as gen (gen.id)}
<details class="mb-4 bg-surface-800 rounded overflow-hidden" on:toggle={(e) => { if ((e.currentTarget as HTMLDetailsElement).open) loadGeneration(gen.id); }}>
<summary class="flex justify-between items-center px-4 py-3 cursor-pointer">
<div class="font-semibold">{gen.name}</div>
<div class="text-surface-400 text-sm">{genData[gen.id]?.loaded ? `${genData[gen.id].pokemon.length} Pokémon` : genData[gen.id]?.loading ? 'Loading…' : 'Open to load'}</div>
</summary>
{#if genData[gen.id]?.loading}
<div class="p-6 text-center text-sm text-surface-400">Loading {gen.name}</div>
{:else if genData[gen.id]?.loaded}
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4 p-4">
{#each genData[gen.id].pokemon as p (p.id)}
<DexEntry id={p.id} name={p.name} defaultImage={p.image} on:change={onTileChange} />
{/each}
</div>
{:else}
<div class="p-6 text-center text-sm text-surface-400">Open the generation to load Pokémon.</div>
{/if}
</details>
{/each}
</div>
</div>

View File

@@ -0,0 +1,117 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { get } from 'svelte/store';
// reuse DexEntry for upload preview
import DexEntry from '$lib/components/DexEntry.svelte';
// route param
const params = get(page).params;
const numberStr = params.number;
const pokeId = Number(numberStr);
let loading = true;
let name: string = '';
let defaultImage: string | null = null;
// user-editable fields
let description: string = '';
let stats: { hp?: number; attack?: number; defense?: number; [k: string]: any } = {};
let customImage: string | null = null;
const storageKey = `dex-${pokeId}`;
onMount(async () => {
loading = true;
try {
// fetch basic pokemon info
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokeId}`);
if (!res.ok) throw new Error('Not found');
const data = await res.json();
name = data.name;
defaultImage = data.sprites?.other?.['official-artwork']?.front_default || data.sprites?.front_default || null;
// load saved user edits from localStorage
const saved = localStorage.getItem(storageKey);
if (saved) {
const parsed = JSON.parse(saved);
description = parsed.description ?? '';
stats = parsed.stats ?? {};
customImage = parsed.imageSrc ?? null;
}
} catch (err) {
console.error(err);
name = `#${pokeId}`;
} finally {
loading = false;
}
});
function onImageChange(e: CustomEvent) {
customImage = e.detail.imageSrc;
}
function save() {
const payload = { name, description, stats, imageSrc: customImage };
localStorage.setItem(storageKey, JSON.stringify(payload));
alert('Saved locally');
}
function resetAll() {
if (confirm('Reset custom image, stats and description for this entry?')) {
localStorage.removeItem(storageKey);
description = '';
stats = {};
customImage = null;
}
}
</script>
{#if loading}
<div class="p-6">Loading pokedex entry #{pokeId}</div>
{:else}
<div class="max-w-4xl mx-auto p-6">
<div class="flex gap-6">
<div class="w-80">
<DexEntry editable={true} id={pokeId} name={name} defaultImage={customImage ?? defaultImage} on:change={onImageChange} />
</div>
<div class="flex-1">
<h1 class="text-2xl font-bold mb-2">{name} <span class="text-sm text-surface-400">#{pokeId}</span></h1>
<div class="mb-4">
<label for="desc-{pokeId}" class="block text-sm font-medium mb-1">Description</label>
<textarea id="desc-{pokeId}" bind:value={description} rows={6} class="w-full p-2 bg-surface-800 rounded"></textarea>
</div>
<div class="mb-4 grid grid-cols-2 gap-3">
<div>
<label for="hp-{pokeId}" class="block text-sm font-medium mb-1">HP</label>
<input id="hp-{pokeId}" type="number" bind:value={stats.hp} class="w-full p-2 bg-surface-800 rounded" />
</div>
<div>
<label for="atk-{pokeId}" class="block text-sm font-medium mb-1">Attack</label>
<input id="atk-{pokeId}" type="number" bind:value={stats.attack} class="w-full p-2 bg-surface-800 rounded" />
</div>
<div>
<label for="def-{pokeId}" class="block text-sm font-medium mb-1">Defense</label>
<input id="def-{pokeId}" type="number" bind:value={stats.defense} class="w-full p-2 bg-surface-800 rounded" />
</div>
<div>
<label for="spd-{pokeId}" class="block text-sm font-medium mb-1">Speed</label>
<input id="spd-{pokeId}" type="number" bind:value={stats.speed} class="w-full p-2 bg-surface-800 rounded" />
</div>
</div>
<div class="flex gap-3">
<button class="px-4 py-2 bg-green-600 rounded text-white" on:click={save}>Save</button>
<button class="px-4 py-2 bg-red-600 rounded text-white" on:click={resetAll}>Reset</button>
</div>
</div>
</div>
</div>
{/if}
<style>
textarea { min-height: 120px; }
</style>

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

14
svelte.config.js Executable file
View File

@@ -0,0 +1,14 @@
import { mdsvex } from "mdsvex";
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: [mdsvex({ extensions: ['.svx', '.md'] }), vitePreprocess()],
kit: {
adapter: adapter()
},
extensions: ['.svelte', '.svx', '.md'],
};
export default config;

19
tsconfig.json Executable file
View File

@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

7
vite.config.ts Executable file
View File

@@ -0,0 +1,7 @@
import tailwindcss from "@tailwindcss/vite";
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()]
});