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
|
||||
# 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
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