Initial Code
This commit is contained in:
145
.gitignore
vendored
Normal file → Executable file
145
.gitignore
vendored
Normal file → Executable file
@@ -1,132 +1,25 @@
|
|||||||
# ---> Node
|
node_modules
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Output
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
# Runtime data
|
# OS
|
||||||
pids
|
.DS_Store
|
||||||
*.pid
|
Thumbs.db
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# Env
|
||||||
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.*
|
||||||
.env.test.local
|
!.env.example
|
||||||
.env.production.local
|
!.env.test
|
||||||
.env.local
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
# Vite
|
||||||
.cache
|
vite.config.js.timestamp-*
|
||||||
.parcel-cache
|
vite.config.ts.timestamp-*
|
||||||
|
|
||||||
# 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.*
|
|
||||||
|
|
||||||
|
bun.lock
|
||||||
|
|||||||
40
package.json
Executable file
40
package.json
Executable 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
28
server/server.js
Executable 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
9
src/app.css
Executable 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
13
src/app.d.ts
vendored
Executable 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
12
src/app.html
Executable 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>
|
||||||
105
src/lib/components/DexEntry.svelte
Normal file
105
src/lib/components/DexEntry.svelte
Normal 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>
|
||||||
110
src/lib/components/Footer.svelte
Normal file
110
src/lib/components/Footer.svelte
Normal 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">·</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">·</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">·</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>
|
||||||
64
src/lib/components/NavBar.svelte
Normal file
64
src/lib/components/NavBar.svelte
Normal 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
51
src/lib/ts/Userdb.ts
Executable 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
20
src/lib/ts/db.ts
Executable 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
3
src/routes/+error.svelte
Executable 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
26
src/routes/+layout.svelte
Executable 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
87
src/routes/+page.svelte
Executable 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>
|
||||||
117
src/routes/dex/[number]/+page.svelte
Normal file
117
src/routes/dex/[number]/+page.svelte
Normal 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
BIN
static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
14
svelte.config.js
Executable file
14
svelte.config.js
Executable 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
19
tsconfig.json
Executable 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
7
vite.config.ts
Executable 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()]
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user