commit 8e1df9281350bbf3a276878de16af5f4cbb849af Author: Nikola Petrov Date: Tue Mar 17 13:41:37 2026 +0100 Init Copy project from personal website diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ec9818 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +output/ +*.sqlite \ No newline at end of file diff --git a/backend/app.ts b/backend/app.ts new file mode 100644 index 0000000..cb50581 --- /dev/null +++ b/backend/app.ts @@ -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(); \ No newline at end of file diff --git a/backend/controllers/mediaController.ts b/backend/controllers/mediaController.ts new file mode 100644 index 0000000..ce1ef18 --- /dev/null +++ b/backend/controllers/mediaController.ts @@ -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 +}; diff --git a/backend/controllers/userController.ts b/backend/controllers/userController.ts new file mode 100644 index 0000000..f8893f3 --- /dev/null +++ b/backend/controllers/userController.ts @@ -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); + }, +}; \ No newline at end of file diff --git a/backend/miscellaneous/checkAuthenticated.ts b/backend/miscellaneous/checkAuthenticated.ts new file mode 100644 index 0000000..016a62f --- /dev/null +++ b/backend/miscellaneous/checkAuthenticated.ts @@ -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; \ No newline at end of file diff --git a/backend/miscellaneous/db.ts b/backend/miscellaneous/db.ts new file mode 100644 index 0000000..e72afa0 --- /dev/null +++ b/backend/miscellaneous/db.ts @@ -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; \ No newline at end of file diff --git a/backend/models/mediaModel.ts b/backend/models/mediaModel.ts new file mode 100644 index 0000000..ddf1f63 --- /dev/null +++ b/backend/models/mediaModel.ts @@ -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 +}; \ No newline at end of file diff --git a/backend/models/userModel.ts b/backend/models/userModel.ts new file mode 100644 index 0000000..9433af3 --- /dev/null +++ b/backend/models/userModel.ts @@ -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 +}; diff --git a/backend/routes/api/apiRouter.ts b/backend/routes/api/apiRouter.ts new file mode 100644 index 0000000..b9144a9 --- /dev/null +++ b/backend/routes/api/apiRouter.ts @@ -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; \ No newline at end of file diff --git a/backend/routes/api/mediaRouter.ts b/backend/routes/api/mediaRouter.ts new file mode 100644 index 0000000..a69bc68 --- /dev/null +++ b/backend/routes/api/mediaRouter.ts @@ -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; \ No newline at end of file diff --git a/backend/routes/main.ts b/backend/routes/main.ts new file mode 100644 index 0000000..7d6604d --- /dev/null +++ b/backend/routes/main.ts @@ -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; \ No newline at end of file diff --git a/backend/routes/user.ts b/backend/routes/user.ts new file mode 100644 index 0000000..877a0d1 --- /dev/null +++ b/backend/routes/user.ts @@ -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; \ No newline at end of file diff --git a/backend/views/user.hbs b/backend/views/user.hbs new file mode 100644 index 0000000..17a835e --- /dev/null +++ b/backend/views/user.hbs @@ -0,0 +1,69 @@ + + + + + + + + + + + + + User + + + + +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
Get
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..cc196c7 --- /dev/null +++ b/build.sh @@ -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/ \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a052487 --- /dev/null +++ b/bun.lock @@ -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=="], + } +} diff --git a/frontend/elementcreate.tsx b/frontend/elementcreate.tsx new file mode 100644 index 0000000..a59a7f0 --- /dev/null +++ b/frontend/elementcreate.tsx @@ -0,0 +1,71 @@ +/// +/// +/// + +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; +} \ No newline at end of file diff --git a/frontend/list/elements.tsx b/frontend/list/elements.tsx new file mode 100644 index 0000000..8e9b06f --- /dev/null +++ b/frontend/list/elements.tsx @@ -0,0 +1,35 @@ +import type { Attributes } from "frontend/elementcreate"; +import * as elements from "frontend/elementcreate"; + +function MediaElement(attributes: Attributes, contents: string[]) { + const ret =
+
+ +
+
{attributes['title']}
+

{attributes['released']}

+
+
+ +
+
+
+
+
; + return ret; +} + + +function MyHeader(attributes: Attributes, contents: string[]) { + return
+
+

{attributes['title']} {attributes['num'] ? ": " + attributes['num'] : ""}

+
+
; +} + +function MediaContainer(attributes: Attributes, contents: string[]) { + return
{contents[0]}
; +} + +export { MediaElement, MyHeader, MediaContainer } \ No newline at end of file diff --git a/frontend/list/functions.tsx b/frontend/list/functions.tsx new file mode 100644 index 0000000..4f82060 --- /dev/null +++ b/frontend/list/functions.tsx @@ -0,0 +1,25 @@ + +function splitByTitle(movies: Array): { [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): { [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 }; \ No newline at end of file diff --git a/frontend/list/list.tsx b/frontend/list/list.tsx new file mode 100644 index 0000000..01ec4d9 --- /dev/null +++ b/frontend/list/list.tsx @@ -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 = []; +var sortButtons: Array = []; + +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): { [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) { + 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(); + + for (const letter of years) { + + const movies = splitMovies[letter]; + + const header = ; + root.appendChild(header); + + const row = + + {movies.map(movie => { + const med = ; + movieElements.push(med); + return med; + })} + ; + + root.appendChild(row); + } +} \ No newline at end of file diff --git a/frontend/list/types.d.ts b/frontend/list/types.d.ts new file mode 100644 index 0000000..241d6b8 --- /dev/null +++ b/frontend/list/types.d.ts @@ -0,0 +1,7 @@ +interface Movie { + title: string; + released: string; + code: string; + webImg: string; + id: string; +} \ No newline at end of file diff --git a/frontend/utils/attr.d.ts b/frontend/utils/attr.d.ts new file mode 100644 index 0000000..3d06107 --- /dev/null +++ b/frontend/utils/attr.d.ts @@ -0,0 +1 @@ +type AttributeValue = number | string | EventListener; \ No newline at end of file diff --git a/frontend/utils/element-types.d.ts b/frontend/utils/element-types.d.ts new file mode 100644 index 0000000..932fc8c --- /dev/null +++ b/frontend/utils/element-types.d.ts @@ -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 \ No newline at end of file diff --git a/frontend/utils/events.d.ts b/frontend/utils/events.d.ts new file mode 100644 index 0000000..ef4ad00 --- /dev/null +++ b/frontend/utils/events.d.ts @@ -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 \ No newline at end of file diff --git a/frontend/utils/intrinsic-elements.d.ts b/frontend/utils/intrinsic-elements.d.ts new file mode 100644 index 0000000..0e66229 --- /dev/null +++ b/frontend/utils/intrinsic-elements.d.ts @@ -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 \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..952995f --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..0718338 --- /dev/null +++ b/public/index.html @@ -0,0 +1,74 @@ + + + + + + + + + + + + + List + + + + +
+ +
+ +
+
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/public/logo.ico b/public/logo.ico new file mode 100644 index 0000000..8df7ca3 Binary files /dev/null and b/public/logo.ico differ diff --git a/public/no_poster.jpg b/public/no_poster.jpg new file mode 100644 index 0000000..5befbc8 Binary files /dev/null and b/public/no_poster.jpg differ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..437d3b8 --- /dev/null +++ b/tsconfig.json @@ -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": "./" + } +} \ No newline at end of file