Copy project from personal website
This commit is contained in:
2026-03-17 13:41:37 +01:00
commit 8e1df92813
29 changed files with 1989 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
output/
*.sqlite

29
backend/app.ts Normal file
View File

@@ -0,0 +1,29 @@
import express from "express";
const hostname = '127.0.0.1';
const httpPort = 4080;
const app = express();
// app.set('views', 'views');
// app.set('view engine', 'hbs');
// import morgan from 'morgan'
// app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static('public'));
import mainRouter from './routes/main';
import apiRouter from './routes/api/apiRouter';
app.use('/', mainRouter);
app.use('/api', apiRouter);
app.listen(httpPort, () => {
console.log(`Server running at http://${hostname}:${httpPort}/`);
});
import mediaController from "./controllers/mediaController";
await mediaController.checkImages();

View File

@@ -0,0 +1,246 @@
import { type Request, type Response } from "express";
import UserModel, { values } from '../models/userModel';
import MediaModel, { Table, Media } from '../models/mediaModel';
import mediaModel from "../models/mediaModel";
interface omdbRes {
Title: string,
Released: string,
Response: string,
Poster: string,
Type: string,
imdbID: string,
Year: string,
}
function fromStringToTable(value: string): (Table | undefined) {
if (value.localeCompare("games") == 0) return Table.games;
if (value.localeCompare("movies") == 0) return Table.movies;
if (value.localeCompare("series") == 0) return Table.series;
return;
}
async function downloadImage(mData: Media, type: Table) {
// Specify the path where you want to save the image
const outputPath = '/poster/' + type + '/' + mData.code + '.jpg';
// Use Bun's built-in fetch to download the image
const response = await fetch(mData.poster);
// Check if the request was successful
if (!response.ok) {
console.log("fetch image error");
console.log(mData.title);
return;
}
// Convert the response to a blob
const imageBlob = await response.blob();
// Use Bun's write to save the image to a file
await Bun.write('./public/' + outputPath, await imageBlob.arrayBuffer());
MediaModel.updateWebImg(type, mData.code, outputPath);
}
async function createMed(req: Request, res: Response) {
const mediaCode: string = req.body.code;
const omdb_key = UserModel.getValue(values.omdb_key);
if (!omdb_key) {
return res.status(500).json({ message: 'Error when creating media' });
}
try {
const uri = `http://www.omdbapi.com/?i=${mediaCode}&apikey=${omdb_key}`;
const mJson = await fetch(uri);
const mData: omdbRes = await mJson.json();
if (mData.Response == 'False') {
return res.status(404).json({ message: 'wrong code' });
}
const media: Media = {
id: 0,
code: mData.imdbID,
title: mData.Title,
released: mData.Released,
webImg: "",
poster: mData.Poster,
year: mData.Year
};
var tableType = Table.series;
if (mData.Type.localeCompare("movie") == 0) {
tableType = Table.movies;
}
const found = MediaModel.findOne(tableType, mediaCode);
if (found.length != 0) {
res.status(409).json({ message: 'Media already exists' });
await downloadImage(media, tableType);
return;
}
const savedMedia = MediaModel.save(tableType, mData.imdbID, mData.Title, mData.Released, "", mData.Poster, mData.Year);
await downloadImage(media, tableType);
res.status(201).json(media);
} catch (err) {
return res.status(500).json({ message: 'Error when creating media' });
}
}
async function createGame(req: Request, res: Response) {
var gameCode = req.body.code;
const twitch_client_id = UserModel.getValue(values.twitch_client_id);
const twitch_client_secret = UserModel.getValue(values.twitch_client_secret);
if (!twitch_client_id || !twitch_client_secret) {
return res.status(500).json({ message: 'Error when creating game' });
}
try {
const gameFound = MediaModel.findOne(Table.games, gameCode);
if (gameFound) {
return res.status(409).json({ message: 'Game already exists' });
}
const uri = "https://id.twitch.tv/oauth2/token?client_id=" + twitch_client_id + "&client_secret=" + twitch_client_secret + "&grant_type=client_credentials";
var response = await fetch(uri, { method: 'POST' });
const mData = await response.json();
const mheaders: HeadersInit = {
'Accept': 'application/json',
'Client-ID': twitch_client_id,
'Authorization': 'Bearer ' + mData.access_token
}
gameCode = parseInt(gameCode)
response = await fetch(
"https://api.igdb.com/v4/games",
{
method: 'POST',
headers: mheaders,
body: `fields name, first_release_date; where id = ${gameCode};`
}
)
const gameData = await response.json()
if (gameData.length == 0) {
return res.status(404).json({ message: 'wrong code' });
}
const date = new Date(gameData[0].first_release_date * 1000);
const options: Intl.DateTimeFormatOptions = { day: 'numeric', month: 'short', year: 'numeric' }
const dateStr = date.toLocaleDateString(undefined, options);
response = await fetch(
"https://api.igdb.com/v4/covers",
{
method: 'POST',
headers: mheaders,
body: `fields image_id; where game = ${gameCode};`
}
)
const coverData = await response.json()
const game: Media = {
id: 0,
code: gameCode,
title: gameData[0].name,
released: dateStr,
webImg: "",
poster: `https://images.igdb.com/igdb/image/upload/t_cover_big/${coverData[0].image_id}.jpg`,
year: date.getFullYear().toString(),
};
const savedGame = MediaModel.save(Table.games, game.code, game.title, game.released, game.webImg, game.poster, game.year);
await downloadImage(game, Table.games);
return res.status(201).json(game);
} catch (error) {
return res.status(500).json({ message: 'Error when creating game', error: error });
}
}
function list(req: Request, res: Response) {
const mediaTable = fromStringToTable(req.params.mediaType);
if (!mediaTable) {
return res.status(500).json({
message: 'Error when getting media.'
});
}
const media = MediaModel.find(mediaTable);
return res.json(media);
}
async function create(req: Request, res: Response) {
const mediaCode: string = req.body.code;
if (mediaCode.startsWith("t")) {
return await createMed(req, res);
} else {
return await createGame(req, res);
}
}
function remove(req: Request, res: Response) {
const mediaTable = fromStringToTable(req.params.mediaType);
if (!mediaTable) {
return res.status(500).json({
message: 'Error when deleting the media.'
});
}
const code = req.body.code;
try {
const mediaTable = req.baseUrl.includes('movies') ? Table.movies : Table.series;
const media = MediaModel.findOneAndDelete(mediaTable, code);
if (!media) {
return res.status(404).json({ message: 'No such media' });
}
return res.status(204).json();
}
catch (err) {
return res.status(500).json({ message: 'Error when deleting the media.' });
}
}
async function checkImages() {
await checkTableImages(Table.games);
await checkTableImages(Table.movies);
await checkTableImages(Table.series);
}
function delay(time:number) {
return new Promise(resolve => setTimeout(resolve, time));
}
async function checkTableImages(table: Table) {
const list = mediaModel.find(table);
for (const element of list) {
const path = "./public/" + element.webImg;
const f = Bun.file(path);
const exists = await f.exists();
if (!exists){
console.log(element.title);
await downloadImage(element, table);
await delay(1000);
}
}
}
export default {
list,
create,
remove,
checkImages
};

View File

@@ -0,0 +1,50 @@
import { type Request, type Response } from "express";
import UserModel, { values } from '../models/userModel';
export default {
render: function (req: Request, res: Response) {
res.render('user', { keys: UserModel.namesOfValues });
},
create: function (req: Request, res: Response) {
const reqPassword: string = req.body.reqPassword;
if (!reqPassword) return res.render('user', { keys: UserModel.namesOfValues });
const password = UserModel.getValue(values.pass);
// if no password in db save reqPassword
if (!password) {
const affectedRows = UserModel.updateValue("pass", reqPassword);
if (affectedRows > 0) {
return res.redirect('/list');
}
return res.render('user', { keys: UserModel.namesOfValues });
}
// check if passwords equal
if (password != reqPassword) {
return res.render('user', { keys: UserModel.namesOfValues });
}
// update
const name: string = req.body.name;
const value: string = req.body.value;
if (!name || !value) {
return res.render('user', { keys: UserModel.namesOfValues });
}
const affectedRows = UserModel.updateValue(name, value);
if (affectedRows == 0) {
return res.render('user', { keys: UserModel.namesOfValues });
}
return res.redirect('/list');
},
get: function (req: Request, res: Response) {
const usersFound = UserModel.getAll();
return res.status(200).json(usersFound);
},
};

View File

@@ -0,0 +1,15 @@
import { type NextFunction, type Request, type Response } from "express";
import userModel, { values } from 'backend/models/userModel';
function checkAuthenticated(req: Request, res: Response, next: NextFunction) {
const pass = req.body.pass;
const password = userModel.getValue(values.pass);
if (pass && password) {
if (pass == password) {
return next();
}
}
return res.status(500).json({ message: 'Error when getting transactions.' });
}
export default checkAuthenticated;

View File

@@ -0,0 +1,68 @@
import { Database } from "bun:sqlite";
const pool = new Database("mydb.sqlite", { strict: true });
pool.run(`
CREATE TABLE IF NOT EXISTS series (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL,
title TEXT NOT NULL,
released TEXT NOT NULL,
webImg TEXT NOT NULL,
poster TEXT NOT NULL,
year TEXT NOT NULL
);
`);
pool.run(`
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL,
title TEXT NOT NULL,
released TEXT NOT NULL,
webImg TEXT NOT NULL,
poster TEXT NOT NULL,
year TEXT NOT NULL
);
`);
pool.run(`
CREATE TABLE IF NOT EXISTS games (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL,
title TEXT NOT NULL,
released TEXT NOT NULL,
webImg TEXT NOT NULL,
poster TEXT NOT NULL,
year TEXT NOT NULL
);
`);
pool.run(`
CREATE TABLE IF NOT EXISTS userData (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
value TEXT NOT NULL
);
`);
function inset_keys() {
class co {
count!: number;
}
const result = pool.query("SELECT count(*) as count FROM userData;").as(co).get();
if(result && result.count >= 4){
return;
}
pool.run(`
INSERT INTO userData (name, value) VALUES ("pass", "");
INSERT INTO userData (name, value) VALUES ("omdb_key", "");
INSERT INTO userData (name, value) VALUES ("twitch_client_id", "");
INSERT INTO userData (name, value) VALUES ("twitch_client_secret", "");
`);
}
inset_keys();
export default pool;

View File

@@ -0,0 +1,84 @@
import pool from 'backend/miscellaneous/db'
export class Media {
id!: number;
code!: string
title!: string;
released!: string;
webImg!: string;
poster!: string;
year!: string;
}
export enum Table {
movies = "movies",
series = "series",
games = "games",
}
function save(table: Table, code: string, title: string, released:string, webImg:string, poster: string, year: string): number {
try {
const sql = "INSERT INTO " + table + " (code, title, released, webImg, poster, year) VALUES (?,?,?,?,?,?)";
const result = pool.query(sql).run(code, title, released, webImg, poster, year);
return result.changes;
}
catch (err) {
console.log(err);
}
return 0;
}
function updateWebImg(table: Table, code: string, webImg: string): number {
try {
const sql = "UPDATE " + table + " SET webImg = ? WHERE code = ?;";
const result = pool.query(sql).run(webImg, code);
return result.changes;
}
catch (err) {
console.log(err);
}
return 0;
}
function findOneAndDelete(table: Table, code: string): number {
try {
const result = pool.query("DELETE FROM " + table + " WHERE code = ?;").run(code);
return result.changes;
}
catch (err) {
console.log(err);
}
return 0;
}
function findOne(table: Table, code: string): Media[] {
try {
const rows = pool.query("SELECT * FROM " + table + " WHERE code = ?;").as(Media).all(code);
return rows;
}
catch (err) {
console.log(err);
}
return [];
}
function find(table: Table): Media[] {
try {
const rows = pool.query("SELECT * FROM " + table + ";").as(Media).all();
return rows;
}
catch (err) {
console.log(err);
}
return [];
}
export default {
save,
updateWebImg,
findOneAndDelete,
findOne,
find
};

View File

@@ -0,0 +1,56 @@
import pool from 'backend/miscellaneous/db'
class UserD {
name?: string;
value?: string;
}
export enum values {
pass = 1,
omdb_key,
twitch_client_id,
twitch_client_secret,
}
const namesOfValues: string[] = ["", "pass", "omdb_key", "twitch_client_id", "twitch_client_secret"];
function getValue(name: values): string | undefined {
try {
const rows = pool.query("SELECT name, value FROM userData where id = ?;").as(UserD).all(name);
if (rows.length > 0)
return rows[0].value;
}
catch (err) {
console.log(err);
}
return;
}
function updateValue(name: string, value: string): number {
try {
const result = pool.query("UPDATE userData SET value = ? WHERE name = ?").run(value, name);
return result.changes;
}
catch (err) {
console.log(err);
}
return 0;
}
function getAll(): UserD[] {
try {
const rows = pool.query("SELECT name, value FROM userData;").as(UserD).all();
return rows;
}
catch (err) {
console.log(err);
}
return [];
}
export default {
getValue,
updateValue,
getAll,
namesOfValues
};

View File

@@ -0,0 +1,12 @@
import express, { type Request, type Response } from "express";
import mediaRouter from './mediaRouter';
const router = express.Router();
router.use('/media', mediaRouter);
router.get('/', function (req: Request, res: Response) {
res.status(200).json({ message: 'API is working' });
});
export default router;

View File

@@ -0,0 +1,13 @@
import express from "express";
import mediaController from '../../controllers/mediaController.js';
import checkAuthenticated from '../../miscellaneous/checkAuthenticated.js';
const router = express.Router();
router.get('/:mediaType', mediaController.list);
router.post('/:mediaType', checkAuthenticated, mediaController.create);
router.delete('/:mediaType', checkAuthenticated, mediaController.remove);
export default router;

10
backend/routes/main.ts Normal file
View File

@@ -0,0 +1,10 @@
import express, { type Request, type Response } from "express";
const router = express.Router();
//import userRouter from './user';
//router.use('/user', userRouter);
export default router;

14
backend/routes/user.ts Normal file
View File

@@ -0,0 +1,14 @@
import express from "express";
import userController from 'backend/controllers/userController';
import checkAuthenticated from 'backend/miscellaneous/checkAuthenticated';
const router = express.Router();
/* GET home page. */
router.get('/', userController.render);
router.post('/', userController.create);
router.put('/', checkAuthenticated, userController.get);
export default router;

69
backend/views/user.hbs Normal file
View File

@@ -0,0 +1,69 @@
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/images/logo.ico" type="image/x-icon">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
<title>User</title>
</head>
<body>
<div class="container">
<form action="/user" method="post">
<div class="mb-3">
<label class="form-label">Name</label>
<select name="name" class="form-select">
{{#each keys}}
<option value="{{this}}">{{this}}</option>
{{/each}}
</select>
</div>
<div class="mb-3">
<label class="form-label">Value</label>
<input type="text" class="form-control" name="value">
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" class="form-control" name="reqPassword" id="password">
</div>
<button type="submit" class="btn btn-primary">Add</button>
</form>
<br>
<div class="btn btn-primary" id="get" onclick="getUser()"> Get</div>
<br>
<div id="text"></div>
<script>
async function getUser() {
const password = document.getElementById('password').value;
const response = await fetch('/user', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ "pass": password }),
})
const data = await response.json();
document.getElementById('text').textContent = JSON.stringify(data, null, 2);
}
</script>
</div>
</body>
</html>

6
build.sh Executable file
View File

@@ -0,0 +1,6 @@
bun build ./backend/app.ts --outfile=output/app.js --target=bun --minify
bun build ./frontend/list/list.tsx --outfile=output/public/list.js --minify
cp -r backend/views/ output/
cp -r public/* output/public/
cp mydb.sqlite output/

209
bun.lock Normal file
View File

@@ -0,0 +1,209 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "web",
"dependencies": {
"@types/express": "^5.0.3",
"@types/morgan": "^1.9.10",
"bun-types": "^1.2.22",
"express": "^5.1.0",
"hbs": "^4.2.0",
"morgan": "~1.10.1",
"typescript": "^5.9.2",
},
},
},
"packages": {
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="],
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
"@types/morgan": ["@types/morgan@1.9.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/qs": ["@types/qs@6.15.0", "", {}, "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
"@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
"foreachasync": ["foreachasync@3.0.0", "", {}, "sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"handlebars": ["handlebars@4.7.7", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.0", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hbs": ["hbs@4.2.0", "", { "dependencies": { "handlebars": "4.7.7", "walk": "2.3.15" } }, "sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"walk": ["walk@2.3.15", "", { "dependencies": { "foreachasync": "^3.0.0" } }, "sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg=="],
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
}
}

View File

@@ -0,0 +1,71 @@
/// <reference path="./utils/element-types.d.ts" />
/// <reference path="./utils/events.d.ts" />
/// <reference path="./utils/intrinsic-elements.d.ts" />
export interface Children {
children?: AttributeValue;
}
export interface CustomElementHandler {
(attributes: Attributes, contents: (string | HTMLElement)[]): HTMLElement;
}
export interface Attributes {
[key: string]: AttributeValue;
}
export function createElement(
tag: string | CustomElementHandler,
attrs: Attributes & Children | undefined = {},
...children: (string | HTMLElement)[]
): HTMLElement {
if (typeof tag === "function") {
if (attrs == null) {
attrs = { num: 0 };
}
if (children == null) {
children = [""];
}
return tag(attrs, children);
}
const retElement = document.createElement(tag);
for (let name in attrs) {
if (name && attrs.hasOwnProperty(name)) {
let value = attrs[name];
if (typeof value === "number") {
retElement.setAttribute(name, value.toString());
} else if (typeof value === "function") {
retElement.addEventListener(name.slice(2), value);
}
else {
retElement.setAttribute(name, value);
}
}
}
for (let i = 2; i < arguments.length; i++) {
let child = arguments[i];
// check if child is a HTMLElement
if (child.nodeType != undefined) {
retElement.appendChild(child);
continue;
}
if (child instanceof Array) {
for (let j = 0; j < child.length; j++) {
if (child[j].nodeType != undefined) retElement.appendChild(child[j]);
else retElement.appendChild(document.createTextNode(child[j].toString()));
}
continue;
}
// child is a string
retElement.appendChild(document.createTextNode(child.toString()));
}
return retElement;
}

View File

@@ -0,0 +1,35 @@
import type { Attributes } from "frontend/elementcreate";
import * as elements from "frontend/elementcreate";
function MediaElement(attributes: Attributes, contents: string[]) {
const ret = <div class="col media-element" id={attributes['id']}>
<div class='card shadow-sm'>
<img class='card-img-top' src={attributes['webImg']} width='100%' onerror={attributes['imageError']} loading="lazy" style="min-height: 100px;"></img>
<div class='card-body'>
<h5 class='card-title'>{attributes['title']}</h5>
<p class='card-text'>{attributes['released']}</p>
<div class="d-none justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick={attributes['fun']}>delete</button>
</div>
</div>
</div>
</div>
</div>;
return ret;
}
function MyHeader(attributes: Attributes, contents: string[]) {
return <div class="row">
<div class='col'>
<h2 class='text-center'>{attributes['title']} {attributes['num'] ? ": " + attributes['num'] : ""}</h2>
</div>
</div>;
}
function MediaContainer(attributes: Attributes, contents: string[]) {
return <div id={attributes['id']} class="row row-cols-2 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 g-3">{contents[0]}</div>;
}
export { MediaElement, MyHeader, MediaContainer }

View File

@@ -0,0 +1,25 @@
function splitByTitle(movies: Array<Movie>): { [s: string]: Movie[]; } {
const result = movies.reduce((r, a) => {
var letter = a.title[0].toUpperCase();
if (!isNaN(parseInt(letter))) letter = "#";
r[letter] = r[letter] || [];
r[letter].push(a);
return r;
}, Object.create(null));
return result;
}
function splitByYear(movies: Array<Movie>): { [s: string]: Movie[]; } {
const result = movies.reduce((r, a) => {
const year = new Date(a.released).getFullYear();
r[year] = r[year] || [];
r[year].push(a);
return r;
}, Object.create(null));
return result;
}
export { splitByTitle, splitByYear };

287
frontend/list/list.tsx Normal file
View File

@@ -0,0 +1,287 @@
import { MediaElement, MyHeader, MediaContainer } from "frontend/list/elements";
import { splitByTitle, splitByYear } from "frontend/list/functions";
import * as elements from "frontend/elementcreate";
var sortType = 0;
var listType = 0;
const sortTypeTitle = 0;
const sortTypeYear = 1;
const sortTypeId = 2;
const moviesType = 0;
const gamesType = 1;
const seriesType = 2;
var listButtons: Array<HTMLElement | null> = [];
var sortButtons: Array<HTMLElement | null> = [];
var root: HTMLElement | null;
var editButton: HTMLElement | null;
var movieElements: HTMLElement[] = [];
function getLink(): string {
switch (listType) {
case moviesType:
return "/api/media/movies";
case gamesType:
return "/api/media/games";
case seriesType:
return "/api/media/series";
}
return "/api/media/movies";
}
async function reload() {
try {
const response = await fetch(getLink());
const movies = await response.json();
renderMedias(movies);
} catch (err) {
console.log(err);
}
}
function submitMedia(event: SubmitEvent) {
event.preventDefault();
const pass = document.getElementById("pass") as HTMLInputElement | null;
if (!pass) return;
const input_id = document.getElementById("input_id") as HTMLInputElement | null;
if (!input_id) return;
if (pass.value == "" || input_id.value == "") return;
fetch(getLink(), {
body: JSON.stringify({ pass: pass.value, code: input_id.value }),
headers: { "Content-Type": "application/json" },
method: "POST"
})
.then(async (response) => {
if (response.status != 201) {
const json = await response.json();
console.log(json);
alert(json.message);
return;
}
await reload();
})
.catch(err => {
console.log(err);
});
input_id.value = "";
}
function loadState() {
const searchParams = new URLSearchParams(window.location.search);
if (searchParams.has("listType")) {
switch (searchParams.get("listType")) {
case "movies":
listType = moviesType;
break;
case "series":
listType = seriesType;
break;
case "games":
listType = gamesType;
break;
default:
listType = moviesType;
break;
}
}
if (searchParams.has("sortType")) {
switch (searchParams.get("sortType")) {
case "title":
sortType = sortTypeTitle;
break;
case "year":
sortType = sortTypeYear;
break;
case "id":
sortType = sortTypeId;
break;
default:
sortType = sortTypeTitle;
break;
}
}
}
function changeType(type: number) {
listType = type;
loadPage();
const searchParams = new URLSearchParams(window.location.search);
switch (listType) {
case moviesType:
searchParams.set("listType", "movies");
break;
case gamesType:
searchParams.set("listType", "games");
break;
case seriesType:
searchParams.set("listType", "series");
break;
}
history.replaceState({}, '', window.location.pathname + '?' + searchParams.toString());
}
function changeSort(type: number) {
sortType = type;
loadPage();
const searchParams = new URLSearchParams(window.location.search);
switch (type) {
case sortTypeTitle:
searchParams.set("sortType", "title");
break;
case sortTypeYear:
searchParams.set("sortType", "year");
break;
case sortTypeId:
searchParams.set("sortType", "id");
break;
}
history.replaceState({}, '', window.location.pathname + '?' + searchParams.toString());
}
function splitBySort(movies: Array<Movie>): { [s: string]: Movie[]; } {
switch (sortType) {
case sortTypeYear:
const sorted = movies.sort((a, b) => {
const ay = Date.parse(a.released);
const by = Date.parse(b.released);
return ay - by;
});
return splitByYear(sorted);
case sortTypeId:
movies.sort((a, b) => a.id < b.id ? 1 : -1);
return { "added": movies };
default:
return splitByTitle(movies.sort((a, b) => a.title.localeCompare(b.title)));
}
}
function toggleEdit() {
movieElements.forEach(element => {
const div = element.querySelector(".d-none");
if (!div) return;
div.classList.remove("d-none");
div.classList.add("d-flex");
});
}
document.addEventListener('DOMContentLoaded', async () => {
document.getElementById("myform")?.addEventListener("submit", submitMedia);
listButtons.push(document.getElementById("movieButton"));
listButtons.push(document.getElementById("gameButton"));
listButtons.push(document.getElementById("seriesButton"));
listButtons.forEach((button, index) => button?.addEventListener("click", () => changeType(index)));
sortButtons.push(document.getElementById("titleButton"));
sortButtons.push(document.getElementById("yearButton"));
sortButtons.push(document.getElementById("idButton"));
sortButtons.forEach((button, index) => button?.addEventListener("click", () => changeSort(index)));
editButton = document.getElementById("editButton");
editButton?.addEventListener("click", () => toggleEdit());
loadState();
loadPage();
});
async function loadPage() {
listButtons.forEach(button => button?.classList.remove("active"));
listButtons[listType]?.classList.add("active");
await reload();
}
function removeMedia(evt: Event) {
const password = document.getElementById("pass") as HTMLInputElement | null;
if (!password) return;
if (password.value == "") return;
let elem = evt.target as HTMLElement | null;
while (elem && !elem.classList.contains('media-element')) {
elem = elem.parentElement;
}
if (!elem) return;
const id = elem.id;
fetch(getLink(), {
body: JSON.stringify({ pass: password.value, code: id }),
headers: { "Content-Type": "application/json" },
method: "DELETE"
})
.then(async (response) => {
if (response.status != 204) {
console.log("error");
console.log(response.body);
return;
}
document.getElementById(id)?.remove();
})
.catch(err => {
console.log(err);
});
password.value = "";
}
function onImgError(evt: Event) {
const imgT = evt.target as HTMLImageElement;
imgT.src = "/no_poster.jpg";
console.log(imgT.parentElement?.parentElement?.id);
}
function renderMedias(unsorted_movies: Array<Movie>) {
root = document.getElementById('root');
if (!root) return;
root.innerHTML = "";
movieElements = [];
const splitMovies = splitBySort(unsorted_movies);
let years;
if (sortType == sortTypeTitle) {
years = Object.keys(splitMovies).sort((a, b) => -b.localeCompare(a));
} else {
years = Object.keys(splitMovies).sort((a, b) => b.localeCompare(a));
}
root.appendChild(<MyHeader title={unsorted_movies.length} />);
for (const letter of years) {
const movies = splitMovies[letter];
const header = <MyHeader title={letter} num={movies.length} />;
root.appendChild(header);
const row =
<MediaContainer id={letter}>
{movies.map(movie => {
const med = <MediaElement webImg={movie.webImg} title={movie.title} released={movie.released} id={movie.code} fun={removeMedia} imageError={onImgError}></MediaElement>;
movieElements.push(med);
return med;
})}
</MediaContainer>;
root.appendChild(row);
}
}

7
frontend/list/types.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
interface Movie {
title: string;
released: string;
code: string;
webImg: string;
id: string;
}

1
frontend/utils/attr.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
type AttributeValue = number | string | EventListener;

341
frontend/utils/element-types.d.ts vendored Normal file
View File

@@ -0,0 +1,341 @@
declare namespace JSX {
interface HtmlTag {
accesskey?: string;
class?: string;
contenteditable?: string;
dir?: string;
hidden?: string | boolean;
id?: AttributeValue;
role?: string;
lang?: string;
draggable?: string | boolean;
spellcheck?: string | boolean;
style?: string;
tabindex?: string;
title?: string;
translate?: string | boolean;
}
interface HtmlAnchorTag extends HtmlTag {
href?: string;
target?: string;
download?: string;
ping?: string;
rel?: string;
media?: string;
hreflang?: string;
type?: string;
}
interface HtmlAreaTag extends HtmlTag {
alt?: string;
coords?: string;
shape?: string;
href?: string;
target?: string;
ping?: string;
rel?: string;
media?: string;
hreflang?: string;
type?: string;
}
interface HtmlAudioTag extends HtmlTag {
src?: string;
autobuffer?: string;
autoplay?: string;
loop?: string;
controls?: string;
}
interface BaseTag extends HtmlTag {
href?: string;
target?: string;
}
interface HtmlQuoteTag extends HtmlTag {
cite?: string;
}
interface HtmlBodyTag extends HtmlTag {
}
interface HtmlButtonTag extends HtmlTag {
action?: string;
autofocus?: string;
disabled?: string;
enctype?: string;
form?: string;
method?: string;
name?: string;
novalidate?: string | boolean;
target?: string;
type?: string;
value?: string;
onClick?: Function;
}
interface HtmlDataListTag extends HtmlTag {
}
interface HtmlCanvasTag extends HtmlTag {
width?: string;
height?: string;
}
interface HtmlTableColTag extends HtmlTag {
span?: string;
}
interface HtmlTableSectionTag extends HtmlTag {
}
interface HtmlTableRowTag extends HtmlTag {
}
interface DataTag extends HtmlTag {
value?: string;
}
interface HtmlEmbedTag extends HtmlTag {
src?: string;
type?: string;
width?: string;
height?: string;
}
interface HtmlFieldSetTag extends HtmlTag {
disabled?: string;
form?: string;
name?: string;
}
interface HtmlFormTag extends HtmlTag {
acceptCharset?: string;
action?: string;
autocomplete?: string;
enctype?: string;
method?: string;
name?: string;
novalidate?: string | boolean;
target?: string;
}
interface HtmlHtmlTag extends HtmlTag {
manifest?: string;
}
interface HtmlIFrameTag extends HtmlTag {
src?: string;
srcdoc?: string;
name?: string;
sandbox?: string;
seamless?: string;
width?: string;
height?: string;
}
interface HtmlImageTag extends HtmlTag {
alt?: string;
src?: AttributeValue;
crossorigin?: string;
usemap?: string;
ismap?: string;
width?: string;
height?: string;
}
interface HtmlInputTag extends HtmlTag {
accept?: string;
action?: string;
alt?: string;
autocomplete?: string;
autofocus?: string;
checked?: string | boolean;
disabled?: string | boolean;
enctype?: string;
form?: string;
height?: string;
list?: string;
max?: string;
maxlength?: string;
method?: string;
min?: string;
multiple?: string;
name?: string;
novalidate?: string | boolean;
pattern?: string;
placeholder?: string;
readonly?: string;
required?: string;
size?: string;
src?: string;
step?: string;
target?: string;
type?: string;
value?: string;
width?: string;
}
interface HtmlModTag extends HtmlTag {
cite?: string;
datetime?: string | Date;
}
interface KeygenTag extends HtmlTag {
autofocus?: string;
challenge?: string;
disabled?: string;
form?: string;
keytype?: string;
name?: string;
}
interface HtmlLabelTag extends HtmlTag {
form?: string;
for?: string;
}
interface HtmlLITag extends HtmlTag {
value?: string | number;
}
interface HtmlLinkTag extends HtmlTag {
href?: string;
crossorigin?: string;
rel?: string;
media?: string;
hreflang?: string;
type?: string;
sizes?: string;
integrity?: string;
}
interface HtmlMapTag extends HtmlTag {
name?: string;
}
interface HtmlMetaTag extends HtmlTag {
name?: string;
httpEquiv?: string;
content?: string;
charset?: string;
}
interface HtmlMeterTag extends HtmlTag {
value?: string | number;
min?: string | number;
max?: string | number;
low?: string | number;
high?: string | number;
optimum?: string | number;
}
interface HtmlObjectTag extends HtmlTag {
data?: string;
type?: string;
name?: string;
usemap?: string;
form?: string;
width?: string;
height?: string;
}
interface HtmlOListTag extends HtmlTag {
reversed?: string;
start?: string | number;
}
interface HtmlOptgroupTag extends HtmlTag {
disabled?: string;
label?: string;
}
interface HtmlOptionTag extends HtmlTag {
disabled?: string;
label?: string;
selected?: string;
value?: string;
}
interface HtmlOutputTag extends HtmlTag {
for?: string;
form?: string;
name?: string;
}
interface HtmlParamTag extends HtmlTag {
name?: string;
value?: string;
}
interface HtmlProgressTag extends HtmlTag {
value?: string | number;
max?: string | number;
}
interface HtmlCommandTag extends HtmlTag {
type?: string;
label?: string;
icon?: string;
disabled?: string;
checked?: string;
radiogroup?: string;
default?: string;
}
interface HtmlLegendTag extends HtmlTag {
}
interface HtmlBrowserButtonTag extends HtmlTag {
type?: string;
}
interface HtmlMenuTag extends HtmlTag {
type?: string;
label?: string;
}
interface HtmlScriptTag extends HtmlTag {
src?: string;
type?: string;
charset?: string;
async?: string;
defer?: string;
crossorigin?: string;
integrity?: string;
text?: string;
}
interface HtmlDetailsTag extends HtmlTag {
open?: string;
}
interface HtmlSelectTag extends HtmlTag {
autofocus?: string;
disabled?: string;
form?: string;
multiple?: string;
name?: string;
required?: string;
size?: string;
}
interface HtmlSourceTag extends HtmlTag {
src?: string;
type?: string;
media?: string;
}
interface HtmlStyleTag extends HtmlTag {
media?: string;
type?: string;
disabled?: string;
scoped?: string;
}
interface HtmlTableTag extends HtmlTag {
}
interface HtmlTableDataCellTag extends HtmlTag {
colspan?: string | number;
rowspan?: string | number;
headers?: string;
}
interface HtmlTextAreaTag extends HtmlTag {
autofocus?: string;
cols?: string;
dirname?: string;
disabled?: string;
form?: string;
maxlength?: string;
minlength?: string;
name?: string;
placeholder?: string;
readonly?: string;
required?: string;
rows?: string;
wrap?: string;
}
interface HtmlTableHeaderCellTag extends HtmlTag {
colspan?: string | number;
rowspan?: string | number;
headers?: string;
scope?: string;
}
interface HtmlTimeTag extends HtmlTag {
datetime?: string | Date;
}
interface HtmlTrackTag extends HtmlTag {
default?: string;
kind?: string;
label?: string;
src?: string;
srclang?: string;
}
interface HtmlVideoTag extends HtmlTag {
src?: string;
poster?: string;
autobuffer?: string;
autoplay?: string;
loop?: string;
controls?: string;
width?: string;
height?: string;
}
}
//# sourceMappingURL=element-types.d.ts.map

98
frontend/utils/events.d.ts vendored Normal file
View File

@@ -0,0 +1,98 @@
declare namespace JSX {
interface HtmlBodyTag {
onafterprint?: string;
onbeforeprint?: string;
onbeforeonload?: string;
onblur?: string;
onerror?: string;
onfocus?: string;
onhaschange?: string;
onload?: string;
onmessage?: string;
onoffline?: string;
ononline?: string;
onpagehide?: string;
onpageshow?: string;
onpopstate?: string;
onredo?: string;
onresize?: string;
onstorage?: string;
onundo?: string;
onunload?: string;
}
interface HtmlTag {
oncontextmenu?: string;
onkeydown?: string;
onkeypress?: string;
onkeyup?: string;
onclick?: AttributeValue;
ondblclick?: string;
ondrag?: string;
ondragend?: string;
ondragenter?: string;
ondragleave?: string;
ondragover?: string;
ondragstart?: string;
ondrop?: string;
onmousedown?: string;
onmousemove?: string;
onmouseout?: string;
onmouseover?: string;
onmouseup?: string;
onmousewheel?: string;
onscroll?: string;
}
interface FormEvents {
onblur?: string;
onchange?: string;
onfocus?: string;
onformchange?: string;
onforminput?: string;
oninput?: string;
oninvalid?: string;
onselect?: string;
onsubmit?: string;
}
interface HtmlInputTag extends FormEvents {
}
interface HtmlFieldSetTag extends FormEvents {
}
interface HtmlFormTag extends FormEvents {
}
interface MediaEvents {
onabort?: string;
oncanplay?: string;
oncanplaythrough?: string;
ondurationchange?: string;
onemptied?: string;
onended?: string;
onerror?: AttributeValue;
onloadeddata?: string;
onloadedmetadata?: string;
onloadstart?: string;
onpause?: string;
onplay?: string;
onplaying?: string;
onprogress?: string;
onratechange?: string;
onreadystatechange?: string;
onseeked?: string;
onseeking?: string;
onstalled?: string;
onsuspend?: string;
ontimeupdate?: string;
onvolumechange?: string;
onwaiting?: string;
}
interface HtmlAudioTag extends MediaEvents {
}
interface HtmlEmbedTag extends MediaEvents {
}
interface HtmlImageTag extends MediaEvents {
}
interface HtmlObjectTag extends MediaEvents {
}
interface HtmlVideoTag extends MediaEvents {
}
}
//# sourceMappingURL=events.d.ts.map

118
frontend/utils/intrinsic-elements.d.ts vendored Normal file
View File

@@ -0,0 +1,118 @@
declare namespace JSX {
type Element = HTMLElement;
interface IntrinsicElements {
a: HtmlAnchorTag;
abbr: HtmlTag;
address: HtmlTag;
area: HtmlAreaTag;
article: HtmlTag;
aside: HtmlTag;
audio: HtmlAudioTag;
b: HtmlTag;
bb: HtmlBrowserButtonTag;
base: BaseTag;
bdi: HtmlTag;
bdo: HtmlTag;
blockquote: HtmlQuoteTag;
body: HtmlBodyTag;
br: HtmlTag;
button: HtmlButtonTag;
canvas: HtmlCanvasTag;
caption: HtmlTag;
cite: HtmlTag;
code: HtmlTag;
col: HtmlTableColTag;
colgroup: HtmlTableColTag;
commands: HtmlCommandTag;
data: DataTag;
datalist: HtmlDataListTag;
dd: HtmlTag;
del: HtmlModTag;
details: HtmlDetailsTag;
dfn: HtmlTag;
div: HtmlTag;
dl: HtmlTag;
dt: HtmlTag;
em: HtmlTag;
embed: HtmlEmbedTag;
fieldset: HtmlFieldSetTag;
figcaption: HtmlTag;
figure: HtmlTag;
footer: HtmlTag;
form: HtmlFormTag;
h1: HtmlTag;
h2: HtmlTag;
h3: HtmlTag;
h4: HtmlTag;
h5: HtmlTag;
h6: HtmlTag;
head: HtmlTag;
header: HtmlTag;
hr: HtmlTag;
html: HtmlHtmlTag;
i: HtmlTag;
iframe: HtmlIFrameTag;
img: HtmlImageTag;
input: HtmlInputTag;
ins: HtmlModTag;
kbd: HtmlTag;
keygen: KeygenTag;
label: HtmlLabelTag;
legend: HtmlLegendTag;
li: HtmlLITag;
link: HtmlLinkTag;
main: HtmlTag;
map: HtmlMapTag;
mark: HtmlTag;
menu: HtmlMenuTag;
meta: HtmlMetaTag;
meter: HtmlMeterTag;
nav: HtmlTag;
noscript: HtmlTag;
object: HtmlObjectTag;
ol: HtmlOListTag;
optgroup: HtmlOptgroupTag;
option: HtmlOptionTag;
output: HtmlOutputTag;
p: HtmlTag;
param: HtmlParamTag;
pre: HtmlTag;
progress: HtmlProgressTag;
q: HtmlQuoteTag;
rb: HtmlTag;
rp: HtmlTag;
rt: HtmlTag;
rtc: HtmlTag;
ruby: HtmlTag;
s: HtmlTag;
samp: HtmlTag;
script: HtmlScriptTag;
section: HtmlTag;
select: HtmlSelectTag;
small: HtmlTag;
source: HtmlSourceTag;
span: HtmlTag;
strong: HtmlTag;
style: HtmlStyleTag;
sub: HtmlTag;
sup: HtmlTag;
table: HtmlTableTag;
tbody: HtmlTag;
td: HtmlTableDataCellTag;
template: HtmlTag;
textarea: HtmlTextAreaTag;
tfoot: HtmlTableSectionTag;
th: HtmlTableHeaderCellTag;
thead: HtmlTableSectionTag;
time: HtmlTimeTag;
title: HtmlTag;
tr: HtmlTableRowTag;
track: HtmlTrackTag;
u: HtmlTag;
ul: HtmlTag;
var: HtmlTag;
video: HtmlVideoTag;
wbr: HtmlTag;
}
}
//# sourceMappingURL=intrinsic-elements.d.ts.map

14
package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "web",
"version": "0.0.0",
"private": true,
"dependencies": {
"@types/express": "^5.0.3",
"@types/morgan": "^1.9.10",
"express": "^5.1.0",
"hbs": "^4.2.0",
"morgan": "~1.10.1",
"bun-types": "^1.2.22",
"typescript": "^5.9.2"
}
}

74
public/index.html Normal file
View File

@@ -0,0 +1,74 @@
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="/logo.ico" type="image/x-icon">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>
<title>List</title>
<script src="/list.js"></script>
</head>
<body>
<header>
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<button class="nav-link active" id="movieButton">Movies</button>
</li>
<li class="nav-item">
<button class="nav-link" id="seriesButton">Series</button>
</li>
<li class="nav-item">
<button class="nav-link" id="gameButton">Games</button>
</li>
<li class="nav-item dropdown">
<button class="nav-link dropdown-toggle" id="navbarDropdown" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Sort
</button>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li><button class="dropdown-item" id="titleButton">Title</button></li>
<li><button class="dropdown-item" id="yearButton">Year</button></li>
<li><button class="dropdown-item" id="idButton">Added</button></li>
</ul>
</li>
<li class="nav-item">
<button class="nav-link" id="editButton">Edit</button>
</li>
</ul>
<form class="d-flex" action="" id="myform">
<input class="form-control me-2" type="password" name="password" id="pass" placeholder="password">
<input class="form-control me-2" type="text" name="input_id" id="input_id" placeholder="id">
<input class="btn btn-outline-success" type="submit" value="Submit">
</form>
</div>
</div>
</nav>
</header>
<main>
<div class="py-5 bg-body-tertiary mt-3">
<div class="container" id="root">
</div>
</div>
</main>
</body>
</html>

BIN
public/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
public/no_poster.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
// Enable latest features
"lib": [
"ESNext",
"DOM"
],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react",
"jsxFactory": "elements.createElement",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"resolveJsonModule": true,
"esModuleInterop": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"types": [
"bun-types"
],
"baseUrl": "./"
}
}