wip new thing

This commit is contained in:
Trit0 2026-02-15 16:09:25 -05:00
parent fddcd1fecd
commit 6a994cee31
50 changed files with 1179 additions and 930 deletions

View File

@ -1,20 +0,0 @@
/* eslint-env node */
// require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-prettier/skip-formatting',
'plugin:@typescript-eslint/recommended'
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
parser: '@typescript-eslint/parser'
},
plugins: [
'@typescript-eslint/eslint-plugin'
]
}

44
eslint.config.mjs Normal file
View File

@ -0,0 +1,44 @@
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import vue from 'eslint-plugin-vue';
import prettier from '@vue/eslint-config-prettier';
import globals from 'globals';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...vue.configs['flat/vue3-essential'],
// This should be last to override other formatting rules
prettier,
{
languageOptions: {
ecmaVersion: 2022,
sourceType: 'module',
globals: {
...globals.browser,
...globals.node,
// Vue compiler macros
'defineProps': 'readonly',
'defineEmits': 'readonly',
'defineExpose': 'readonly',
'withDefaults': 'readonly'
}
}
},
{
// Limit linting to specific file types
files: ['src/**/*.vue', 'src/**/*.js', 'src/**/*.ts'],
},
{
// Ignore files from linting
ignores: [
'node_modules',
'dist',
'src/**/*.d.ts',
'*.config.js',
'*.config.cjs'
],
}
);

270
package-lock.json generated
View File

@ -40,6 +40,7 @@
"sass": "^1.68.0", "sass": "^1.68.0",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.55.0",
"vite": "^5.4.14", "vite": "^5.4.14",
"vitest": "^3.0.8", "vitest": "^3.0.8",
"vue-tsc": "^2.2.2" "vue-tsc": "^2.2.2"
@ -522,9 +523,9 @@
} }
}, },
"node_modules/@eslint-community/regexpp": { "node_modules/@eslint-community/regexpp": {
"version": "4.12.1", "version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0" "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
@ -1707,20 +1708,19 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.24.1", "version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz",
"integrity": "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==", "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/scope-manager": "8.55.0",
"@typescript-eslint/type-utils": "8.24.1", "@typescript-eslint/type-utils": "8.55.0",
"@typescript-eslint/utils": "8.24.1", "@typescript-eslint/utils": "8.55.0",
"@typescript-eslint/visitor-keys": "8.24.1", "@typescript-eslint/visitor-keys": "8.55.0",
"graphemer": "^1.4.0", "ignore": "^7.0.5",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"ts-api-utils": "^2.0.1" "ts-api-utils": "^2.4.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1730,22 +1730,31 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "@typescript-eslint/parser": "^8.55.0",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0" "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"engines": {
"node": ">= 4"
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.24.1", "version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz",
"integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==", "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/scope-manager": "8.55.0",
"@typescript-eslint/types": "8.24.1", "@typescript-eslint/types": "8.55.0",
"@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/typescript-estree": "8.55.0",
"@typescript-eslint/visitor-keys": "8.24.1", "@typescript-eslint/visitor-keys": "8.55.0",
"debug": "^4.3.4" "debug": "^4.4.3"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1756,17 +1765,38 @@
}, },
"peerDependencies": { "peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0" "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz",
"integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.55.0",
"@typescript-eslint/types": "^8.55.0",
"debug": "^4.4.3"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.24.1", "version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz",
"integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==", "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.24.1", "@typescript-eslint/types": "8.55.0",
"@typescript-eslint/visitor-keys": "8.24.1" "@typescript-eslint/visitor-keys": "8.55.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1776,16 +1806,33 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz",
"integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.24.1", "version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz",
"integrity": "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==", "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/types": "8.55.0",
"@typescript-eslint/utils": "8.24.1", "@typescript-eslint/typescript-estree": "8.55.0",
"debug": "^4.3.4", "@typescript-eslint/utils": "8.55.0",
"ts-api-utils": "^2.0.1" "debug": "^4.4.3",
"ts-api-utils": "^2.4.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1796,13 +1843,13 @@
}, },
"peerDependencies": { "peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0" "typescript": ">=4.8.4 <6.0.0"
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.24.1", "version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz",
"integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1813,19 +1860,20 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.24.1", "version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz",
"integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.24.1", "@typescript-eslint/project-service": "8.55.0",
"@typescript-eslint/visitor-keys": "8.24.1", "@typescript-eslint/tsconfig-utils": "8.55.0",
"debug": "^4.3.4", "@typescript-eslint/types": "8.55.0",
"fast-glob": "^3.3.2", "@typescript-eslint/visitor-keys": "8.55.0",
"is-glob": "^4.0.3", "debug": "^4.4.3",
"minimatch": "^9.0.4", "minimatch": "^9.0.5",
"semver": "^7.6.0", "semver": "^7.7.3",
"ts-api-utils": "^2.0.1" "tinyglobby": "^0.2.15",
"ts-api-utils": "^2.4.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1835,7 +1883,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": ">=4.8.4 <5.8.0" "typescript": ">=4.8.4 <6.0.0"
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
@ -1863,15 +1911,15 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.24.1", "version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz",
"integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==", "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/scope-manager": "8.55.0",
"@typescript-eslint/types": "8.24.1", "@typescript-eslint/types": "8.55.0",
"@typescript-eslint/typescript-estree": "8.24.1" "@typescript-eslint/typescript-estree": "8.55.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1882,17 +1930,17 @@
}, },
"peerDependencies": { "peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0" "typescript": ">=4.8.4 <6.0.0"
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.24.1", "version": "8.55.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz",
"integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.24.1", "@typescript-eslint/types": "8.55.0",
"eslint-visitor-keys": "^4.2.0" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1903,9 +1951,9 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "4.2.0", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2892,9 +2940,9 @@
"dev": true "dev": true
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
}, },
@ -4028,12 +4076,6 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true "dev": true
}, },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -5444,9 +5486,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.1", "version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true, "dev": true,
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@ -5854,6 +5896,51 @@
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"dev": true "dev": true
}, },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tinypool": { "node_modules/tinypool": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
@ -5920,9 +6007,9 @@
} }
}, },
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.0.1", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
"integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=18.12" "node": ">=18.12"
@ -5984,14 +6071,15 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.24.1", "version": "8.55.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.1.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.0.tgz",
"integrity": "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA==", "integrity": "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/eslint-plugin": "8.55.0",
"@typescript-eslint/parser": "8.24.1", "@typescript-eslint/parser": "8.55.0",
"@typescript-eslint/utils": "8.24.1" "@typescript-eslint/typescript-estree": "8.55.0",
"@typescript-eslint/utils": "8.55.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -6002,7 +6090,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.8.0" "typescript": ">=4.8.4 <6.0.0"
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {

View File

@ -7,7 +7,7 @@
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "vite preview", "preview": "vite preview",
"test:unit": "vitest", "test:unit": "vitest",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", "lint": "eslint . --fix",
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
@ -43,6 +43,7 @@
"sass": "^1.68.0", "sass": "^1.68.0",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.55.0",
"vite": "^5.4.14", "vite": "^5.4.14",
"vitest": "^3.0.8", "vitest": "^3.0.8",
"vue-tsc": "^2.2.2" "vue-tsc": "^2.2.2"

View File

@ -1,23 +1,23 @@
<script setup> <script setup>
import {RouterLink, RouterView, useRoute} from 'vue-router' import { RouterLink, RouterView, useRoute } from 'vue-router'
import {storeToRefs} from 'pinia'; import { storeToRefs } from 'pinia'
import {useAuthStore} from '@/stores/auth'; import { useAuthStore } from '@/stores/auth'
import router from '@/router'; import router from '@/router'
import Errors from '@/components/Errors.vue' import Errors from '@/components/Errors.vue'
import {ref} from 'vue'; import { ref } from 'vue'
const authStr = useAuthStore() const authStr = useAuthStore()
const {auth} = storeToRefs(authStr) const { auth } = storeToRefs(authStr)
</script> </script>
<template> <template>
<RouterView/> <RouterView />
</template> </template>
<style> <style>
footer { footer {
padding: 2rem 0; padding: 2rem 0;
} }
</style> </style>

View File

@ -1,18 +1,18 @@
/* color palette from <https://github.com/vuejs/theme> */ /* color palette from <https://github.com/vuejs/theme> */
:root { :root {
--vt-c-white: #FDFDFF; --vt-c-white: #fdfdff;
--vt-c-white-soft: #f8f8f8; --vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2; --vt-c-white-mute: #f2f2f2;
--vt-c-black: #171A1B; --vt-c-black: #171a1b;
--vt-c-black-soft: #2C2F31; --vt-c-black-soft: #2c2f31;
--vt-c-black-mute: #393D3F; --vt-c-black-mute: #393d3f;
--vt-c-payne: 135, 151, 163; --vt-c-payne: 135, 151, 163;
--vt-c-bittersweet: #C14953; --vt-c-bittersweet: #c14953;
--vt-c-silver: #C6C5B9; --vt-c-silver: #c6c5b9;
--vt-c-munsell: #8EC5CC; --vt-c-munsell: #8ec5cc;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29); --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12); --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
@ -23,7 +23,6 @@
--vt-c-text-light-2: rgba(60, 60, 60, 0.66); --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white); --vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64); --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
} }
/* semantic color variables for this project */ /* semantic color variables for this project */
@ -91,4 +90,4 @@ body {
} }
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@ -1,47 +1,34 @@
@import 'base.css'; @import 'base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
gap: 1rem;
font-weight: normal;
}
a.router-link-active { a.router-link-active {
color: var(--vt-c-munsell) !important; color: var(--vt-c-munsell) !important;
} }
a { a {
text-decoration: none; text-decoration: none;
color: var(--vt-c-silver); color: var(--vt-c-silver);
transition: 0.4s; transition: 0.4s;
} }
@media (hover: hover) { @media (hover: hover) {
/*a:hover,*/ /*a:hover,*/
/*button:hover {*/ /*button:hover {*/
/* background-color: var(--vt-c-payne);*/ /* background-color: var(--vt-c-payne);*/
/*}*/ /*}*/
} }
/* Example CSS for headings with additional styles */ /* Example CSS for headings with additional styles */
h1 { h1 {
font-size: 2rem; font-size: 2rem;
line-height: 4rem line-height: 4rem;
} }
h2 { h2 {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 3rem; line-height: 3rem;
} }
h3 { h3 {
font-size: 1.2rem; font-size: 1.2rem;
line-height: 2.4rem; line-height: 2.4rem;
} }

View File

@ -18,10 +18,7 @@
@input="updateDate" @input="updateDate"
></v-text-field> ></v-text-field>
</template> </template>
<v-date-picker <v-date-picker :model-value="getDate" @update:modelValue="updateDate"></v-date-picker>
:model-value="getDate"
@update:modelValue="updateDate"
></v-date-picker>
</v-menu> </v-menu>
</div> </div>
</template> </template>
@ -36,18 +33,18 @@ export default {
value: { value: {
type: String, type: String,
default() { default() {
return "" return ''
}, }
}, }
}, },
data() { data() {
return { return {
menu: false, menu: false
}; }
}, },
computed: { computed: {
dateFormatted() { dateFormatted() {
return this.input ? new Date(this.input) : ""; return this.input ? new Date(this.input) : ''
}, },
getDate() { getDate() {
const date = this.input ? new Date(this.input) : new Date() const date = this.input ? new Date(this.input) : new Date()
@ -59,16 +56,16 @@ export default {
console.log(val) console.log(val)
}, },
updateDate(val) { updateDate(val) {
this.menu = false; this.menu = false
console.log(val) console.log(val)
this.input = val this.input = val
}, }
}, }
}; }
</script> </script>
<style scoped> <style scoped>
.v-text-field input { .v-text-field input {
background-color: transparent !important; background-color: transparent !important;
} }
</style> </style>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { useErrorStore } from '@/stores/errors' import { useErrorStore } from '@/stores/errors'
import {storeToRefs} from "pinia"; import { storeToRefs } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
let isOpen = ref(false) let isOpen = ref(false)
@ -21,7 +21,7 @@ const { errors } = storeToRefs(errorStore)
type="submit" type="submit"
@click="isOpen = !isOpen" @click="isOpen = !isOpen"
> >
{{ errors.length }} error(s) {{ errors.length }} error(s)
</button> </button>
</section> </section>
</template> </template>

32
src/components/Header.vue Normal file
View File

@ -0,0 +1,32 @@
<script setup>
import { RouterLink } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import router from '@/router'
import { LogOutIcon } from 'lucide-vue-next'
const authStore = useAuthStore()
const logout = () => {
authStore.set(null)
router.push('/login')
}
</script>
<template>
<header class="flex h-16 items-center justify-between border-b px-4">
<div class="flex items-center gap-4">
<RouterLink to="/">
<img alt="Conjure logo" class="h-8" src="@/assets/logo_conjure_dark.png" />
</RouterLink>
</div>
<div class="flex items-center gap-4">
<button
@click="logout"
class="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground"
>
<LogOutIcon class="h-5 w-5" />
Logout
</button>
</div>
</header>
</template>

View File

@ -10,9 +10,7 @@ defineProps({
<template> <template>
<div class="greetings"> <div class="greetings">
<h1 class="green">{{ msg }}</h1> <h1 class="green">{{ msg }}</h1>
<h3> <h3>Youre successfully a foken 🍞 in the ass.</h3>
Youre successfully a foken 🍞 in the ass.
</h3>
</div> </div>
</template> </template>

View File

@ -161,7 +161,6 @@ defineProps({
</template> </template>
<style scoped> <style scoped>
svg { svg {
margin: auto; margin: auto;
} }

View File

@ -0,0 +1,51 @@
<template>
<aside :class="[' text-foreground p-4 transition-all duration-300 border-r', isCollapsed ? 'w-16' : 'w-64']">
<div class="flex items-center" :class="isCollapsed ? 'justify-center' : 'justify-end'">
<button @click="toggleCollapse" class="p-2 rounded-full hover:bg-muted">
<PanelLeft v-if="!isCollapsed" class="h-6 w-6" />
<PanelRight v-if="isCollapsed" class="h-6 w-6" />
</button>
</div>
<nav class="mt-8">
<div v-if="isAdmin" class="rounded-lg hover:bg-muted transition-colors duration-200 p-2">
<RouterLink
to="/users"
class="flex items-center"
:title="isCollapsed ? 'Users' : ''"
>
<Users class="h-6 w-6" />
<span v-if="!isCollapsed" class="ml-4">Users</span>
</RouterLink>
</div>
<!-- Other links can go here -->
</nav>
</aside>
</template>
<script setup>
import { ref } from 'vue';
import { RouterLink } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
import { storeToRefs } from 'pinia';
import { PanelLeft, PanelRight, Users } from 'lucide-vue-next';
const isCollapsed = ref(false);
function toggleCollapse() {
isCollapsed.value = !isCollapsed.value;
}
const authStore = useAuthStore();
const { auth } = storeToRefs(authStore);
const isAdmin = ref(auth.value?.role?.label === 'Admin');
</script>
<style scoped>
/* Add any specific styles for the sidebar here */
.router-link-exact-active {
background-color: hsl(var(--muted-foreground) / 0.2);
}
</style>

View File

@ -9,83 +9,82 @@ import SupportIcon from './icons/IconSupport.vue'
<template> <template>
<article> <article>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
<WelcomeItem> Vues
<template #icon> <a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
<DocumentationIcon /> provides you with all information you need to get started.
</template> </WelcomeItem>
<template #heading>Documentation</template>
Vues <WelcomeItem>
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a> <template #icon>
provides you with all information you need to get started. <ToolingIcon />
</WelcomeItem> </template>
<template #heading>Tooling</template>
<WelcomeItem> This project is served and bundled with
<template #icon> <a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
<ToolingIcon /> recommended IDE setup is
</template> <a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
<template #heading>Tooling</template> <a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
<a href="https://on.cypress.io/component" target="_blank" rel="noopener"
>Cypress Component Testing</a
>.
This project is served and bundled with <br />
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
<a href="https://on.cypress.io/component" target="_blank" rel="noopener"
>Cypress Component Testing</a
>.
<br /> More instructions are available in <code>README.md</code>.
</WelcomeItem>
More instructions are available in <code>README.md</code>. <WelcomeItem>
</WelcomeItem> <template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
<WelcomeItem> Get official tools and libraries for your project:
<template #icon> <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<EcosystemIcon /> <a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
</template> <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<template #heading>Ecosystem</template> <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>.
If you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
Get official tools and libraries for your project: <WelcomeItem>
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>, <template #icon>
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>, <CommunityIcon />
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and </template>
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If <template #heading>Community</template>
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem> Got stuck? Ask your question on
<template #icon> <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
<CommunityIcon /> Discord server, or
</template> <a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
<template #heading>Community</template> >StackOverflow</a
>. You should also subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and
follow the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
Got stuck? Ask your question on <WelcomeItem>
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official <template #icon>
Discord server, or <SupportIcon />
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener" </template>
>StackOverflow</a <template #heading>Support Vue</template>
>. You should also subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem> As an independent project, Vue relies on community backing for its sustainability. You can
<template #icon> help us by
<SupportIcon /> <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</template> </WelcomeItem>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</article> </article>
</template> </template>

View File

@ -1,11 +0,0 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})

View File

@ -1,9 +1,9 @@
export class AuthDto { export class AuthDto {
email: string; email: string
password: string; password: string
public constructor(form: FormData) { public constructor(form: FormData) {
this.email = form.get("email") as string; this.email = form.get('email') as string
this.password = form.get("password") as string; this.password = form.get('password') as string
} }
} }

View File

@ -1,50 +1,48 @@
export class UploadGameDto { export class UploadGameDto {
title: string; title: string
description: string; description: string
medias: Blob[]; medias: Blob[]
filesBlob: Blob; filesBlob: Blob
files?: string; files?: string
genres: string[]; genres: string[]
developers: string[]; developers: string[]
publicRepositoryLink: string; publicRepositoryLink: string
players: string; players: string
release: string; release: string
modification: string; modification: string
version: string; version: string
leaderboard: boolean; leaderboard: boolean
thumbnail?: string;
image?: string;
thumbnail?: string
image?: string
constructor(formData: FormData, mediaFileList: FileList) { constructor(formData: FormData, mediaFileList: FileList) {
this.title = formData.get("title") as string; this.title = formData.get('title') as string
this.description = formData.get("description") as string; this.description = formData.get('description') as string
this.publicRepositoryLink = formData.get("repo") as string; this.publicRepositoryLink = formData.get('repo') as string
this.players = formData.get("players") as string; this.players = formData.get('players') as string
this.release = new Date(formData.get("release") as string).toISOString().split("T")[0]; this.release = new Date(formData.get('release') as string).toISOString().split('T')[0]
this.modification = new Date().toISOString().split("T")[0]; this.modification = new Date().toISOString().split('T')[0]
this.version = formData.get("version") as string; this.version = formData.get('version') as string
this.leaderboard = !!formData.get("leaderboard"); this.leaderboard = !!formData.get('leaderboard')
this.medias = [] this.medias = []
for (let i = 0; i < mediaFileList.length; i++) { for (let i = 0; i < mediaFileList.length; i++) {
this.medias[i] = mediaFileList[i]; this.medias[i] = mediaFileList[i]
} }
this.filesBlob = formData.get("game") as Blob; this.filesBlob = formData.get('game') as Blob
this.genres = (formData.get("genres") as string).split(",").map(s => s.trim()); this.genres = (formData.get('genres') as string).split(',').map((s) => s.trim())
this.developers = (formData.get("devs") as string).split(",").map(s => s.trim()); this.developers = (formData.get('devs') as string).split(',').map((s) => s.trim())
} }
async setMediasAndFiles() { async setMediasAndFiles() {
if (this.medias.length > 0) { if (this.medias.length > 0) {
this.thumbnail = await this.medias[0].text(); this.thumbnail = await this.medias[0].text()
} }
if (this.medias.length > 1) { if (this.medias.length > 1) {
this.image = await this.medias[1].text(); this.image = await this.medias[1].text()
} }
this.files = await this.filesBlob.text(); this.files = await this.filesBlob.text()
} }
}
}

View File

@ -1,39 +1,43 @@
export class ValidateMetadataDto { export class ValidateMetadataDto {
id: string; id: string
game: string; game: string
description: string; description: string
files: string; files: string
genres: string[]; genres: string[]
developers: string[]; developers: string[]
publicRepositoryLink: string; publicRepositoryLink: string
players: string; players: string
release: string; release: string
modification: string; modification: string
version: string; version: string
leaderboard: boolean; leaderboard: boolean
unityLibraryVersion?: string; unityLibraryVersion?: string
thumbnail: number[]; thumbnail: number[]
image: number[]; image: number[]
constructor(
constructor(formData: FormData, thumbnail: Uint8Array | null, image: Uint8Array | null, filePath: string, id: string) { formData: FormData,
thumbnail: Uint8Array | null,
image: Uint8Array | null,
filePath: string,
id: string
) {
this.id = id this.id = id
this.game = formData.get("title") as string; this.game = formData.get('title') as string
this.description = formData.get("description") as string; this.description = formData.get('description') as string
this.publicRepositoryLink = formData.get("repo") as string; this.publicRepositoryLink = formData.get('repo') as string
this.players = formData.get("players") as string; this.players = formData.get('players') as string
this.release = new Date(formData.get("release") as string).toISOString().split("T")[0]; this.release = new Date(formData.get('release') as string).toISOString().split('T')[0]
this.modification = new Date().toISOString().split("T")[0]; this.modification = new Date().toISOString().split('T')[0]
this.version = formData.get("version") as string; this.version = formData.get('version') as string
this.leaderboard = !!formData.get("leaderboard"); this.leaderboard = !!formData.get('leaderboard')
this.thumbnail = !!thumbnail ? Array.from(thumbnail) : []; this.thumbnail = !!thumbnail ? Array.from(thumbnail) : []
this.image = !!image ? Array.from(image): []; this.image = !!image ? Array.from(image) : []
this.files = filePath; this.files = filePath
this.genres = (formData.get("genres") as string).split(",").map(s => s.trim().toLowerCase()); this.genres = (formData.get('genres') as string).split(',').map((s) => s.trim().toLowerCase())
this.developers = (formData.get("devs") as string).split(",").map(s => s.trim()); this.developers = (formData.get('devs') as string).split(',').map((s) => s.trim())
} }
}
}

View File

@ -1,6 +1,11 @@
export interface AuthResponse { export interface Role {
id: number; id: number
roles: string[]; label: string
token: string; }
username: string;
export interface AuthResponse {
id: number
role: Role
token: string
email: string
} }

View File

@ -1,7 +1,7 @@
export interface Game { export interface Game {
id: number; id: number
title: string; title: string
description: string; description: string
thumbnail: any; thumbnail: any
active: boolean active: boolean
} }

7
src/interfaces/user.ts Normal file
View File

@ -0,0 +1,7 @@
import { Role } from "./auth"
export interface User {
id: number
email: string
role: Role
}

View File

@ -9,14 +9,13 @@ import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives' import * as directives from 'vuetify/directives'
import DateFnsAdapter from '@date-io/date-fns' import DateFnsAdapter from '@date-io/date-fns'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
const vuetify = createVuetify({ const vuetify = createVuetify({
components, components,
directives, directives,
theme: false, theme: false
}) })
const app = createApp(App) const app = createApp(App)

View File

@ -3,7 +3,6 @@ import HomeView from '../views/HomeView.vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
@ -26,27 +25,28 @@ const router = createRouter({
{ {
path: '/games', path: '/games',
name: 'games', name: 'games',
component: () => import('../views/games/GamesView.vue'), component: () => import('../views/games/GamesView.vue')
}, },
{ {
path: '/upload', path: '/upload',
name: 'upload', name: 'upload',
component: () => import('../views/games/UploadView.vue'), component: () => import('../views/games/UploadView.vue')
}, },
{ {
path: '/manual-upload', path: '/manual-upload',
name: 'manual-upload', name: 'manual-upload',
component: () => import('../views/games/ManualUploadView.vue'), component: () => import('../views/games/ManualUploadView.vue')
},
{
path: '/events',
name: 'events',
component: () => import('../views/mqtt/Events.vue'),
}, },
{ {
path: '/games/:gameId', path: '/games/:gameId',
name: 'game', name: 'game',
component: () => import('../views/games/GameView.vue'), component: () => import('../views/games/GameView.vue')
},
{
path: '/users',
name: 'users',
component: () => import('../views/users/UsersView.vue'),
meta: { requiresAdmin: true }
} }
] ]
}, },
@ -60,12 +60,12 @@ const router = createRouter({
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
component: () => import('../views/auths/Login.vue'), component: () => import('../views/auths/Login.vue')
}, },
{ {
path: '/sign-up', path: '/sign-up',
name: 'sign-up', name: 'sign-up',
component: () => import('../views/auths/SignUp.vue'), component: () => import('../views/auths/SignUp.vue')
} }
] ]
}, },
@ -96,10 +96,27 @@ const router = createRouter({
}) })
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth === undefined) const authStore = useAuthStore()
next() const isLoggedIn = authStore.isAuth()
const userRole = authStore.getAuth()?.role?.label
if (useAuthStore().isAuth() === to.meta.requiresAuth) { // Handle routes that require admin access
if (to.meta.requiresAdmin) {
if (isLoggedIn && userRole === 'Admin') {
next() // Allow access
} else {
next('/games') // Redirect non-admins
}
return
}
// Handle general authentication requirements
if (to.meta.requiresAuth === undefined) {
next()
return
}
if (isLoggedIn === to.meta.requiresAuth) {
// If the condition is met, allow access to the route // If the condition is met, allow access to the route
next() next()
} else if (to.meta.requiresAuth) { } else if (to.meta.requiresAuth) {

View File

@ -2,12 +2,11 @@ import { AuthDto } from '@/dtos/auth.dto'
import { BaseService } from '@/services/base-service' import { BaseService } from '@/services/base-service'
export class AuthService extends BaseService { export class AuthService extends BaseService {
public async login(data: AuthDto): Promise<Response> { public async login(data: AuthDto): Promise<Response> {
return this.post('login', data); return this.post('login', data)
} }
public async signup(dto: AuthDto): Promise<Response> { public async signup(dto: AuthDto): Promise<Response> {
return this.post('signup', dto); return this.post('signup', dto)
} }
} }

View File

@ -1,28 +1,32 @@
export class BaseService { export class BaseService {
protected apiUrl: string | undefined = import.meta.env.VITE_CONJUREOS_HOST protected apiUrl: string | undefined = import.meta.env.VITE_CONJUREOS_HOST
protected baseHeaders: Record<string, string> = { protected baseHeaders: Record<string, string> = {
'Accept': 'application/json', Accept: 'application/json',
'API-Version': '1' 'API-Version': '1'
} }
protected jsonHeaders: Record<string, string> = { protected jsonHeaders: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
} }
public constructor() { public constructor() {}
}
protected async get<T>(path: string, headers?: HeadersInit): Promise<T> { protected async get<T>(path: string, headers?: HeadersInit): Promise<T> {
return await (await fetch(this.apiUrl + path, { return (await (
method: 'GET', await fetch(this.apiUrl + path, {
headers: { method: 'GET',
...headers, headers: {
...this.baseHeaders ...headers,
} ...this.baseHeaders
})).json() as T }
})
).json()) as T
} }
protected async post<T extends object>(path: string, body: T, headers?: HeadersInit): Promise<Response> { protected async post<T extends object>(
path: string,
body: T,
headers?: HeadersInit
): Promise<Response> {
return await fetch(this.apiUrl + path, { return await fetch(this.apiUrl + path, {
method: 'POST', method: 'POST',
body: JSON.stringify(body) as any, body: JSON.stringify(body) as any,
@ -31,7 +35,7 @@ export class BaseService {
...this.jsonHeaders, ...this.jsonHeaders,
...this.baseHeaders ...this.baseHeaders
} }
}); })
} }
protected async postForm(path: string, body: FormData, headers?: HeadersInit): Promise<Response> { protected async postForm(path: string, body: FormData, headers?: HeadersInit): Promise<Response> {
@ -42,7 +46,7 @@ export class BaseService {
...headers, ...headers,
...this.baseHeaders ...this.baseHeaders
} }
}); })
} }
protected async put<T>(path: string, body: T, headers?: HeadersInit): Promise<Response> { protected async put<T>(path: string, body: T, headers?: HeadersInit): Promise<Response> {
@ -54,7 +58,7 @@ export class BaseService {
...this.jsonHeaders, ...this.jsonHeaders,
...this.baseHeaders ...this.baseHeaders
} }
}); })
} }
protected async delete(path: string, headers?: HeadersInit): Promise<Response> { protected async delete(path: string, headers?: HeadersInit): Promise<Response> {
@ -64,6 +68,6 @@ export class BaseService {
...headers, ...headers,
...this.baseHeaders ...this.baseHeaders
} }
}); })
} }
} }

View File

@ -5,23 +5,22 @@ import { useAuthStore } from '@/stores/auth'
import { AuthDto } from '@/dtos/auth.dto' import { AuthDto } from '@/dtos/auth.dto'
export class GameService extends BaseService { export class GameService extends BaseService {
public getGames(): Promise<Game[]> { public getGames(): Promise<Game[]> {
return this.get<Game[]>("games") return this.get<Game[]>('games')
} }
public getGame(gameId: string): Promise<Game> { public getGame(gameId: string): Promise<Game> {
return this.get<Game>(`games/${gameId}`) return this.get<Game>(`games/${gameId}`)
} }
public async upload(game: FormData) : Promise<Response> { public async upload(game: FormData): Promise<Response> {
const authStr = useAuthStore(); const authStr = useAuthStore()
return this.postForm(`games`, game, { return this.postForm(`games`, game, {
Authorization: `Bearer ${authStr.getAuth()?.token}`, Authorization: `Bearer ${authStr.getAuth()?.token}`
}); })
} }
public async update(game: Game) : Promise<void> { public async update(game: Game): Promise<void> {
await this.put<any>(`games`, null) await this.put<any>(`games`, null)
} }
@ -60,4 +59,4 @@ export class GameService extends BaseService {
public async deleteGame(gameId: string): Promise<void> { public async deleteGame(gameId: string): Promise<void> {
await this.delete(`games/${gameId}`) await this.delete(`games/${gameId}`)
} }
} }

View File

@ -0,0 +1,12 @@
import { User } from "@/interfaces/user";
import { BaseService } from "./base-service";
import { useAuthStore } from "@/stores/auth";
export class UserService extends BaseService {
public async getUsers(): Promise<User[]> {
const authStr = useAuthStore()
return this.get<User[]>('user', {
Authorization: `Bearer ${authStr.getAuth()?.token}`
});
}
}

8
src/shims-vue.d.ts vendored
View File

@ -1,6 +1,6 @@
declare module "*.vue" { declare module '*.vue' {
import type {DefineComponent} from "vue"; import type { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const component: DefineComponent<object, object, any>; const component: DefineComponent<object, object, any>
export default component; export default component
} }

View File

@ -4,17 +4,13 @@ import { AuthResponse } from '@/interfaces/auth'
const key = 'AUTH' const key = 'AUTH'
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const localStorageAuth = localStorage.getItem(key) const localStorageAuth = localStorage.getItem(key)
const auth: Ref<AuthResponse | null> = ref(localStorageAuth ? JSON.parse(localStorageAuth) : null) const auth: Ref<AuthResponse | null> = ref(localStorageAuth ? JSON.parse(localStorageAuth) : null)
function set(_auth: AuthResponse) { function set(_auth: AuthResponse) {
auth.value = _auth auth.value = _auth
if (_auth) if (_auth) localStorage.setItem(key, JSON.stringify(_auth))
localStorage.setItem(key, JSON.stringify(_auth)) else localStorage.removeItem(key)
else
localStorage.removeItem(key)
} }
function isAuth() { function isAuth() {

View File

@ -1,20 +1,18 @@
import {defineStore} from "pinia"; import { defineStore } from 'pinia'
import {ref} from "vue"; import { ref } from 'vue'
export const useErrorStore = defineStore('error', () => { export const useErrorStore = defineStore('error', () => {
/** @type {(string[])} */ /** @type {(string[])} */
const errors = ref([] as any[]) const errors = ref([] as any[])
function unshift(error: string) {
console.error('Error:', error)
errors.value.unshift(error)
}
function unshift(error: string) { function getErrors() {
console.error('Error:', error); return errors
errors.value.unshift(error) }
}
return { errors, getErrors, unshift }
function getErrors() {
return errors
}
return { errors, getErrors, unshift}
}) })

View File

@ -1,28 +1,25 @@
import {defineStore} from "pinia"; import { defineStore } from 'pinia'
import {ref} from "vue"; import { ref } from 'vue'
const key = "AUTH" const key = 'AUTH'
export const usePlayerAuthStore = defineStore('auth', () => { export const usePlayerAuthStore = defineStore('auth', () => {
/** @type {(undefined | string | any)} */ /** @type {(undefined | string | any)} */
const auth = ref(JSON.parse(localStorage.getItem(key)) || undefined) const auth = ref(JSON.parse(localStorage.getItem(key)) || undefined)
/** @param {(undefined | string)} auth */
function set(auth) {
this.auth = auth
if (auth) localStorage.setItem(key, auth.toString())
else localStorage.removeItem(key)
}
/** @param {(undefined | string)} auth */ function isAuth() {
function set(auth) { return !!this.auth
this.auth = auth }
if (auth)
localStorage.setItem(key, auth.toString())
else
localStorage.removeItem(key)
}
function isAuth() { function getAuth() {
return !!this.auth return this.auth
} }
function getAuth() { return { auth, getAuth, set, isAuth }
return this.auth
}
return {auth, getAuth, set, isAuth}
}) })

13
src/stores/users.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { User } from '@/interfaces/user'
export const useUsersStore = defineStore('users', () => {
const list = ref([] as User[])
function set(_list: User[]) {
list.value = _list
}
return { list, set }
})

View File

@ -1,8 +1,8 @@
export class BlobUtil { export class BlobUtil {
static getFileName(file: File | Blob): string { static getFileName(file: File | Blob): string {
if (file instanceof File) { if (file instanceof File) {
return file.name; return file.name
} }
return (file as File).name return (file as File).name
} }
} }

View File

@ -1,46 +1,51 @@
export class ConjUtil { export class ConjUtil {
static scoreExecutable(gameTitle: string, fileName: string): number { static scoreExecutable(gameTitle: string, fileName: string): number {
const normalizedTitle = gameTitle.toLowerCase().replace(/[^a-z0-9]/g, ''); // Remove spaces & special chars const normalizedTitle = gameTitle.toLowerCase().replace(/[^a-z0-9]/g, '') // Remove spaces & special chars
const normalizedFile = fileName.toLowerCase().replace(/[^a-z0-9]/g, ''); const normalizedFile = fileName.toLowerCase().replace(/[^a-z0-9]/g, '')
let score = 0; let score = 0
// ✅ High score if exact match (ignoring case & symbols) // ✅ High score if exact match (ignoring case & symbols)
if (normalizedFile === normalizedTitle) score += 100; if (normalizedFile === normalizedTitle) score += 100
// ✅ Reward filenames that contain the game title // ✅ Reward filenames that contain the game title
if (normalizedFile.includes(normalizedTitle)) score += 50; if (normalizedFile.includes(normalizedTitle)) score += 50
// ✅ Favor files that end in the game title // ✅ Favor files that end in the game title
if (normalizedFile.endsWith(normalizedTitle)) score += 25; if (normalizedFile.endsWith(normalizedTitle)) score += 25
// ✅ Favor filenames that are short (avoid engine-related long names) // ✅ Favor filenames that are short (avoid engine-related long names)
score += Math.max(0, 20 - normalizedFile.length / 5); score += Math.max(0, 20 - normalizedFile.length / 5)
// ❌ Penalize common engine-related executables // ❌ Penalize common engine-related executables
const engineFiles = [ const engineFiles = [
"unitycrashhandler", "unrealengine", "unrealeditor", 'unitycrashhandler',
"launcher", "updater", "configtool", "settings" 'unrealengine',
]; 'unrealeditor',
if (engineFiles.some(engine => normalizedFile.includes(engine))) score -= 50; 'launcher',
'updater',
'configtool',
'settings'
]
if (engineFiles.some((engine) => normalizedFile.includes(engine))) score -= 50
return score; return score
} }
static findBestExecutable(gameTitle: string, exeList: string[]): string | null { static findBestExecutable(gameTitle: string, exeList: string[]): string | null {
if (exeList.length === 0) return null; if (exeList.length === 0) return null
let bestMatch = exeList[0]; let bestMatch = exeList[0]
let highestScore = -Infinity; let highestScore = -Infinity
for (const exe of exeList) { for (const exe of exeList) {
const score = this.scoreExecutable(gameTitle, exe); const score = this.scoreExecutable(gameTitle, exe)
if (score > highestScore) { if (score > highestScore) {
highestScore = score; highestScore = score
bestMatch = exe; bestMatch = exe
} }
} }
return bestMatch; return bestMatch
} }
} }

View File

@ -1,17 +1,17 @@
export class FileSaver { export class FileSaver {
static saveFile(file: File) { static saveFile(file: File) {
const a = document.createElement('a'); const a = document.createElement('a')
document.body.appendChild(a); document.body.appendChild(a)
a.style.display = 'none'; a.style.display = 'none'
const url = window.URL.createObjectURL(file); const url = window.URL.createObjectURL(file)
a.href = url; a.href = url
a.download = file.name; a.download = file.name
a.click(); a.click()
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url)
document.body.removeChild(a); document.body.removeChild(a)
} }
static saveBlob(blob: Blob, filename: string) { static saveBlob(blob: Blob, filename: string) {
return this.saveFile(new File([blob], filename)); return this.saveFile(new File([blob], filename))
} }
} }

View File

@ -1,9 +1,9 @@
export class GuidUtil { export class GuidUtil {
static generateUUIDv4(): string { static generateUUIDv4(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
const random = Math.random() * 16 | 0; const random = (Math.random() * 16) | 0
const value = char === 'x' ? random : (random & 0x3 | 0x8); const value = char === 'x' ? random : (random & 0x3) | 0x8
return value.toString(16); return value.toString(16)
}); })
} }
} }

View File

@ -1,147 +1,163 @@
export class ZipUtil { export class ZipUtil {
zipParts: Uint8Array<ArrayBuffer>[] = [] zipParts: Uint8Array<ArrayBuffer>[] = []
centralDirectory: Uint8Array<ArrayBuffer>[] = []; centralDirectory: Uint8Array<ArrayBuffer>[] = []
offset: number = 0; offset: number = 0
private crc32(buffer: Uint8Array): number { private crc32(buffer: Uint8Array): number {
let table = new Uint32Array(256); let table = new Uint32Array(256)
for (let i = 0; i < 256; i++) { for (let i = 0; i < 256; i++) {
let c = i; let c = i
for (let j = 0; j < 8; j++) { for (let j = 0; j < 8; j++) {
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
} }
table[i] = c; table[i] = c
} }
let crc = 0xFFFFFFFF; let crc = 0xffffffff
for (let i = 0; i < buffer.length; i++) { for (let i = 0; i < buffer.length; i++) {
crc = (crc >>> 8) ^ table[(crc ^ buffer[i]) & 0xFF]; crc = (crc >>> 8) ^ table[(crc ^ buffer[i]) & 0xff]
} }
return ~crc >>> 0; return ~crc >>> 0
} }
private createFileHeader(fileName: string, fileData: ArrayBuffer): Uint8Array<ArrayBuffer> { private createFileHeader(fileName: string, fileData: ArrayBuffer): Uint8Array<ArrayBuffer> {
const encodedFileName = new TextEncoder().encode(fileName); const encodedFileName = new TextEncoder().encode(fileName)
const crc = this.crc32(new Uint8Array(fileData)); const crc = this.crc32(new Uint8Array(fileData))
const fileSize = fileData.byteLength; const fileSize = fileData.byteLength
const header = new Uint8Array(30 + encodedFileName.length); const header = new Uint8Array(30 + encodedFileName.length)
const view = new DataView(header.buffer); const view = new DataView(header.buffer)
view.setUint32(0, 0x04034b50, true); // Local file header signature view.setUint32(0, 0x04034b50, true) // Local file header signature
view.setUint16(4, 20, true); // Version view.setUint16(4, 20, true) // Version
view.setUint16(6, 0, true); // General purpose flag view.setUint16(6, 0, true) // General purpose flag
view.setUint16(8, 0, true); // Compression method (0 = no compression) view.setUint16(8, 0, true) // Compression method (0 = no compression)
view.setUint16(10, 0, true); // File modification time view.setUint16(10, 0, true) // File modification time
view.setUint16(12, 0, true); // File modification date view.setUint16(12, 0, true) // File modification date
view.setUint32(14, crc, true); // CRC-32 view.setUint32(14, crc, true) // CRC-32
view.setUint32(18, fileSize, true); // Compressed size view.setUint32(18, fileSize, true) // Compressed size
view.setUint32(22, fileSize, true); // Uncompressed size view.setUint32(22, fileSize, true) // Uncompressed size
view.setUint16(26, encodedFileName.length, true); // Filename length view.setUint16(26, encodedFileName.length, true) // Filename length
view.setUint16(28, 0, true); // Extra field length view.setUint16(28, 0, true) // Extra field length
header.set(encodedFileName, 30); header.set(encodedFileName, 30)
return header; return header
} }
private createCentralDirectoryEntry(fileName: string, fileSize: number, crc: number, fileOffset: number): Uint8Array<ArrayBuffer> { private createCentralDirectoryEntry(
const encodedFileName = new TextEncoder().encode(fileName); fileName: string,
const entry = new Uint8Array(46 + encodedFileName.length); fileSize: number,
const view = new DataView(entry.buffer); crc: number,
fileOffset: number
): Uint8Array<ArrayBuffer> {
const encodedFileName = new TextEncoder().encode(fileName)
const entry = new Uint8Array(46 + encodedFileName.length)
const view = new DataView(entry.buffer)
view.setUint32(0, 0x02014b50, true); // Central file header signature view.setUint32(0, 0x02014b50, true) // Central file header signature
view.setUint16(4, 20, true); // Version view.setUint16(4, 20, true) // Version
view.setUint16(6, 20, true); // Version needed to extract view.setUint16(6, 20, true) // Version needed to extract
view.setUint16(8, 0, true); // General purpose flag view.setUint16(8, 0, true) // General purpose flag
view.setUint16(10, 0, true); // Compression method (0 = no compression) view.setUint16(10, 0, true) // Compression method (0 = no compression)
view.setUint16(12, 0, true); // File modification time view.setUint16(12, 0, true) // File modification time
view.setUint16(14, 0, true); // File modification date view.setUint16(14, 0, true) // File modification date
view.setUint32(16, crc, true); // CRC-32 view.setUint32(16, crc, true) // CRC-32
view.setUint32(20, fileSize, true); // Compressed size view.setUint32(20, fileSize, true) // Compressed size
view.setUint32(24, fileSize, true); // Uncompressed size view.setUint32(24, fileSize, true) // Uncompressed size
view.setUint16(28, encodedFileName.length, true); // Filename length view.setUint16(28, encodedFileName.length, true) // Filename length
view.setUint16(30, 0, true); // Extra field length view.setUint16(30, 0, true) // Extra field length
view.setUint16(32, 0, true); // File comment length view.setUint16(32, 0, true) // File comment length
view.setUint16(34, 0, true); // Disk number start view.setUint16(34, 0, true) // Disk number start
view.setUint16(36, 0, true); // Internal file attributes view.setUint16(36, 0, true) // Internal file attributes
view.setUint32(38, 0, true); // External file attributes view.setUint32(38, 0, true) // External file attributes
view.setUint32(42, fileOffset, true); // Relative offset of local header view.setUint32(42, fileOffset, true) // Relative offset of local header
entry.set(encodedFileName, 46); entry.set(encodedFileName, 46)
return entry; return entry
} }
createEndOfCentralDirectory() { createEndOfCentralDirectory() {
const eocd = new Uint8Array(22); const eocd = new Uint8Array(22)
const view = new DataView(eocd.buffer); const view = new DataView(eocd.buffer)
view.setUint32(0, 0x06054b50, true); // End of central directory signature view.setUint32(0, 0x06054b50, true) // End of central directory signature
view.setUint16(4, 0, true); // Number of this disk view.setUint16(4, 0, true) // Number of this disk
view.setUint16(6, 0, true); // Disk where central directory starts view.setUint16(6, 0, true) // Disk where central directory starts
view.setUint16(8, this.centralDirectory.length, true); // Number of central directory records on this disk view.setUint16(8, this.centralDirectory.length, true) // Number of central directory records on this disk
view.setUint16(10, this.centralDirectory.length, true); // Total number of central directory records view.setUint16(10, this.centralDirectory.length, true) // Total number of central directory records
view.setUint32(12, this.centralDirectory.reduce((sum, entry) => sum + entry.length, 0), true); // Size of central directory view.setUint32(
view.setUint32(16, this.offset, true); // Offset of central directory 12,
view.setUint16(20, 0, true); // Comment length this.centralDirectory.reduce((sum, entry) => sum + entry.length, 0),
return eocd; true
) // Size of central directory
view.setUint32(16, this.offset, true) // Offset of central directory
view.setUint16(20, 0, true) // Comment length
return eocd
} }
async addFile(filePath: string, fileBlob: Blob): Promise<void> { async addFile(filePath: string, fileBlob: Blob): Promise<void> {
const fileData = await fileBlob.arrayBuffer(); const fileData = await fileBlob.arrayBuffer()
const fileHeader = this.createFileHeader(filePath, fileData); const fileHeader = this.createFileHeader(filePath, fileData)
this.zipParts.push(fileHeader, new Uint8Array(fileData)); this.zipParts.push(fileHeader, new Uint8Array(fileData))
const fileOffset = this.offset; const fileOffset = this.offset
this.offset += fileHeader.length + fileData.byteLength; this.offset += fileHeader.length + fileData.byteLength
this.centralDirectory.push(this.createCentralDirectoryEntry(filePath, fileData.byteLength, this.crc32(new Uint8Array(fileData)), fileOffset)); this.centralDirectory.push(
this.createCentralDirectoryEntry(
filePath,
fileData.byteLength,
this.crc32(new Uint8Array(fileData)),
fileOffset
)
)
} }
getZipBlob(): Blob { getZipBlob(): Blob {
this.zipParts.push(...this.centralDirectory, this.createEndOfCentralDirectory()); this.zipParts.push(...this.centralDirectory, this.createEndOfCentralDirectory())
return new Blob(this.zipParts, { type: "application/zip" }); return new Blob(this.zipParts, { type: 'application/zip' })
} }
static async createZipBlob(files: { filePath: string; fileBlob: Blob }[]): Promise<Blob> { static async createZipBlob(files: { filePath: string; fileBlob: Blob }[]): Promise<Blob> {
const util = new ZipUtil(); const util = new ZipUtil()
for (const { filePath, fileBlob } of files) { for (const { filePath, fileBlob } of files) {
await util.addFile(filePath, fileBlob); await util.addFile(filePath, fileBlob)
} }
return util.getZipBlob(); return util.getZipBlob()
} }
static async getZipFileTree(zipBlob: Blob): Promise<string[]> { static async getZipFileTree(zipBlob: Blob): Promise<string[]> {
const fileTree: string[] = []; const fileTree: string[] = []
// Read ZIP Blob into an ArrayBuffer // Read ZIP Blob into an ArrayBuffer
const arrayBuffer = await zipBlob.arrayBuffer(); const arrayBuffer = await zipBlob.arrayBuffer()
const dataView = new DataView(arrayBuffer); const dataView = new DataView(arrayBuffer)
let offset = arrayBuffer.byteLength - 22; // Start near the end of the ZIP file let offset = arrayBuffer.byteLength - 22 // Start near the end of the ZIP file
// Find End of Central Directory Record // Find End of Central Directory Record
while (offset > 0) { while (offset > 0) {
if (dataView.getUint32(offset, true) === 0x06054b50) break; // Signature for End of Central Directory if (dataView.getUint32(offset, true) === 0x06054b50) break // Signature for End of Central Directory
offset--; offset--
} }
if (offset <= 0) throw new Error("Invalid ZIP file: No End of Central Directory found"); if (offset <= 0) throw new Error('Invalid ZIP file: No End of Central Directory found')
// Read the Central Directory Offset // Read the Central Directory Offset
const centralDirOffset = dataView.getUint32(offset + 16, true); const centralDirOffset = dataView.getUint32(offset + 16, true)
offset = centralDirOffset; offset = centralDirOffset
// Parse Central Directory Headers to extract filenames // Parse Central Directory Headers to extract filenames
while (offset < arrayBuffer.byteLength) { while (offset < arrayBuffer.byteLength) {
if (dataView.getUint32(offset, true) !== 0x02014b50) break; // Central Directory File Header if (dataView.getUint32(offset, true) !== 0x02014b50) break // Central Directory File Header
const fileNameLength = dataView.getUint16(offset + 28, true); const fileNameLength = dataView.getUint16(offset + 28, true)
const extraFieldLength = dataView.getUint16(offset + 30, true); const extraFieldLength = dataView.getUint16(offset + 30, true)
const commentLength = dataView.getUint16(offset + 32, true); const commentLength = dataView.getUint16(offset + 32, true)
// Extract the filename // Extract the filename
const fileNameBytes = new Uint8Array(arrayBuffer, offset + 46, fileNameLength); const fileNameBytes = new Uint8Array(arrayBuffer, offset + 46, fileNameLength)
const fileName = new TextDecoder().decode(fileNameBytes); const fileName = new TextDecoder().decode(fileNameBytes)
fileTree.push(fileName); fileTree.push(fileName)
// Move to the next entry // Move to the next entry
offset += 46 + fileNameLength + extraFieldLength + commentLength; offset += 46 + fileNameLength + extraFieldLength + commentLength
} }
return fileTree; return fileTree
} }
} }

View File

@ -8,18 +8,26 @@ import Errors from '@/components/Errors.vue'
const authStr = useAuthStore() const authStr = useAuthStore()
const { auth } = storeToRefs(authStr) const { auth } = storeToRefs(authStr)
</script> </script>
<template> <template>
<header> <header>
<img alt="Conjure logo" class="logo" src="@/assets/logo_conjure_dark.png" width="2228" height="349" /> <img
alt="Conjure logo"
class="logo"
src="@/assets/logo_conjure_dark.png"
width="2228"
height="349"
/>
</header> </header>
<RouterView /> <RouterView />
<footer> <footer>
<router-link v-if="auth" to="/" <router-link
class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded w-full"> v-if="auth"
to="/"
class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded w-full"
>
To dashboard To dashboard
</router-link> </router-link>
<Errors /> <Errors />
@ -27,7 +35,6 @@ const { auth } = storeToRefs(authStr)
</template> </template>
<style scoped> <style scoped>
.logo { .logo {
user-drag: none; user-drag: none;
-webkit-user-drag: none; -webkit-user-drag: none;

View File

@ -1,114 +1,25 @@
<script setup> <script setup>
import {RouterLink, RouterView } from 'vue-router' import { RouterView } from 'vue-router'
import Sidebar from '@/components/Sidebar.vue'
import {storeToRefs} from 'pinia'; import Header from '@/components/Header.vue'
import {useAuthStore} from '@/stores/auth';
import router from '@/router';
import Errors from '@/components/Errors.vue' import Errors from '@/components/Errors.vue'
import { LogOutIcon } from 'lucide-vue-next'
const authStr = useAuthStore()
const {auth} = storeToRefs(authStr)
const logout = () => {
authStr.set(undefined)
router.push('/')
location.reload()
}
</script> </script>
<template> <template>
<header class="flex flex-row justify-between items-center mb-8"> <div class="flex h-screen w-screen bg-muted/40">
<div class="flex flex-row items-center mb-8 gap-10"> <Sidebar />
<RouterLink to="/"> <div class="flex flex-col flex-1">
<img alt="Conjure logo" class="logo max-w-lg" src="@/assets/logo_conjure_dark.png"/> <Header />
</RouterLink> <main class="flex-1 p-8 overflow-y-auto">
<!-- <nav>--> <RouterView />
<!-- <RouterLink to="/games">Games</RouterLink>--> </main>
<!-- <RouterLink to="/upload">Upload</RouterLink>--> <footer class="p-4">
<!-- </nav>--> <Errors />
</footer>
</div> </div>
<button id="logout" class="border-transparent font-bold text-foreground py-2 px-4 border hover:border-primary rounded mb-5" </div>
@click="logout()" v-if="auth">
<LogOutIcon class="m-2" />
</button>
</header>
<RouterView/>
<footer>
<Errors/>
</footer>
</template> </template>
<style scoped> <style scoped>
/* Scoped styles can remain if there are any specific to Member.vue layout */
header {
line-height: 1.5;
max-height: 100dvh;
margin-bottom: 2rem;
}
.logo {
display: block;
margin: 0 auto 2rem;
user-drag: none;
-webkit-user-drag: none;
}
nav {
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
#logout {
display: inline-block;
padding: 0 1rem;
}
nav a:first-of-type {
border: 0;
}
footer {
display: flex;
gap: 1rem;
}
@media (min-width: 1024px) {
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style> </style>

View File

@ -1,19 +1,16 @@
<script setup> <script setup>
import { RouterLink, useRoute, useRouter } from 'vue-router'
import {RouterLink, useRoute, useRouter} from 'vue-router'; const route = useRoute()
const router = useRouter()
const route = useRoute();
const router = useRouter();
function getPathSegment() { function getPathSegment() {
const pathSegments = route.path.split('/').filter(segment => segment !== ''); const pathSegments = route.path.split('/').filter((segment) => segment !== '')
const routes = router.getRoutes().reduce((p, c) => ({...p, [c.path]: true}), {}) const routes = router.getRoutes().reduce((p, c) => ({ ...p, [c.path]: true }), {})
const back = '/'; const back = '/'
for (let i = 1; i < pathSegments.length; i++) { for (let i = 1; i < pathSegments.length; i++) {
const toTest = back + pathSegments.slice(0, -i).join('/'); const toTest = back + pathSegments.slice(0, -i).join('/')
console.log(toTest, routes[toTest]) console.log(toTest, routes[toTest])
if (!routes[toTest]) { if (!routes[toTest]) {
continue continue
@ -23,24 +20,22 @@ function getPathSegment() {
} }
return back return back
} }
</script> </script>
<template> <template>
<article> <article>
<h1> <h1>Shit not found bruh</h1>
Shit not found bruh <RouterLink
</h1> :to="getPathSegment()"
<RouterLink :to="getPathSegment()" class="bg-transparent font-bold text-foreground underline underline-offset-8 hover:no-underline hover:text-foreground py-2 px-4 border border-transparent hover:border-transparent rounded hover:bg-primary"
class="bg-transparent font-bold text-foreground underline underline-offset-8 hover:no-underline hover:text-foreground py-2 px-4 border border-transparent hover:border-transparent rounded hover:bg-primary"> >
go back go back
</RouterLink> </RouterLink>
</article> </article>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
article { article {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -64,5 +59,4 @@ ul {
} }
} }
} }
</style>
</style>

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router' import { RouterLink, RouterView } from 'vue-router'
import Errors from '@/components/Errors.vue' import Errors from '@/components/Errors.vue'
</script> </script>
@ -9,7 +8,13 @@ import Errors from '@/components/Errors.vue'
<div id="auth-content"> <div id="auth-content">
<header> <header>
<RouterLink to="/"> <RouterLink to="/">
<img alt="Conjure logo" class="logo" src="@/assets/logo_conjure_dark.png" width="2228" height="349"/> <img
alt="Conjure logo"
class="logo"
src="@/assets/logo_conjure_dark.png"
width="2228"
height="349"
/>
</RouterLink> </RouterLink>
<nav> <nav>
<RouterLink to="/login">Login</RouterLink> <RouterLink to="/login">Login</RouterLink>
@ -17,15 +22,24 @@ import Errors from '@/components/Errors.vue'
</nav> </nav>
</header> </header>
<RouterView/> <RouterView />
<footer> <footer>
<Errors/> <Errors />
</footer> </footer>
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
#auth {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
gap: 1rem;
font-weight: normal;
}
.logo { .logo {
user-drag: none; user-drag: none;
-webkit-user-drag: none; -webkit-user-drag: none;
@ -112,4 +126,4 @@ footer {
padding: 0 2rem; padding: 0 2rem;
} }
} }
</style> </style>

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { useErrorStore } from '@/stores/errors' import { useErrorStore } from '@/stores/errors'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import router from '@/router' import router from '@/router'
@ -9,24 +8,24 @@ import { AuthService } from '@/services/auth.service'
import { AuthDto } from '@/dtos/auth.dto' import { AuthDto } from '@/dtos/auth.dto'
const errorStore = useErrorStore() const errorStore = useErrorStore()
const authService = new AuthService(); const authService = new AuthService()
const isLoginIn = ref(false) const isLoginIn = ref(false)
const login = async (form: HTMLFormElement) => { const login = async (form: HTMLFormElement) => {
const formData = new FormData(form); const formData = new FormData(form)
isLoginIn.value = true; isLoginIn.value = true
const dto = new AuthDto(formData); const dto = new AuthDto(formData)
const response = await authService.login(dto).catch((error) => { const response = await authService.login(dto).catch((error) => {
isLoginIn.value = false; isLoginIn.value = false
errorStore.unshift(error); errorStore.unshift(error)
}) })
isLoginIn.value = false; isLoginIn.value = false
if (!response) { if (!response) {
return; return
} }
const result = await response.json(); const result = await response.json()
useAuthStore().set(result) useAuthStore().set(result)
router.push('/') router.push('/')
} }
@ -35,7 +34,11 @@ const login = async (form: HTMLFormElement) => {
<template> <template>
<article> <article>
<h1>Login</h1> <h1>Login</h1>
<form ref="loginForm" enctype="multipart/form-data" @submit.prevent="login($refs.loginForm as HTMLFormElement)"> <form
ref="loginForm"
enctype="multipart/form-data"
@submit.prevent="login($refs.loginForm as HTMLFormElement)"
>
<label for="email">Email</label> <label for="email">Email</label>
<input <input
required required

View File

@ -13,7 +13,7 @@ const signup = (form: HTMLFormElement) => {
const formData = new FormData(form) const formData = new FormData(form)
isLoginIn.value = true isLoginIn.value = true
const myHeaders = new Headers() const myHeaders = new Headers()
myHeaders.append('API-Version', "1") myHeaders.append('API-Version', '1')
fetch(apiHost + 'signup', { fetch(apiHost + 'signup', {
method: 'POST', method: 'POST',
body: formData, body: formData,
@ -21,10 +21,9 @@ const signup = (form: HTMLFormElement) => {
}) })
.then((response) => { .then((response) => {
if (response.status !== 200) if (response.status !== 200)
return response.text().then(error => { return response.text().then((error) => {
throw new Error(error) throw new Error(error)
} })
)
return response.text() return response.text()
}) })
.then((result) => { .then((result) => {
@ -38,24 +37,36 @@ const signup = (form: HTMLFormElement) => {
errorStore.unshift(error) errorStore.unshift(error)
}) })
} }
</script> </script>
<template> <template>
<article> <article>
<h1>Sign up</h1> <h1>Sign up</h1>
<form ref="signupForm" enctype="multipart/form-data" @submit.prevent="signup($refs.signupForm as HTMLFormElement)"> <form
ref="signupForm"
enctype="multipart/form-data"
@submit.prevent="signup($refs.signupForm as HTMLFormElement)"
>
<label for="email">Email</label> <label for="email">Email</label>
<input required type="email" <input
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" required
name="email" id="email" /> type="email"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
name="email"
id="email"
/>
<label for="password">Password</label> <label for="password">Password</label>
<input required type="password" <input
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" required
name="password" id="password" /> type="password"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
name="password"
id="password"
/>
<button <button
class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded" class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded"
type="submit"> type="submit"
>
Signup Signup
</button> </button>
</form> </form>
@ -79,4 +90,4 @@ input {
label { label {
margin-bottom: -1.5rem; margin-bottom: -1.5rem;
} }
</style> </style>

View File

@ -1,108 +1,113 @@
<script setup> <script setup>
import {onMounted, ref} from 'vue'; import { onMounted, ref } from 'vue'
import {useErrorStore} from '@/stores/errors' import { useErrorStore } from '@/stores/errors'
import Loader from '@/components/Loader.vue'; import Loader from '@/components/Loader.vue'
import {useRoute} from 'vue-router'; import { useRoute } from 'vue-router'
import {useAuthStore} from '@/stores/auth'; import { useAuthStore } from '@/stores/auth'
import {storeToRefs} from 'pinia'; import { storeToRefs } from 'pinia'
import router from '@/router'; import router from '@/router'
const errorStore = useErrorStore(); const errorStore = useErrorStore()
const authStore = useAuthStore(); const authStore = useAuthStore()
const { auth } = storeToRefs(authStore); const { auth } = storeToRefs(authStore)
const apiHost = import.meta.env.VITE_CONJUREOS_HOST; const apiHost = import.meta.env.VITE_CONJUREOS_HOST
const route = useRoute(); const route = useRoute()
const gameId = ref(route.params.gameId); const gameId = ref(route.params.gameId)
const game = ref(undefined); const game = ref(undefined)
const isActivating = ref(false); const isActivating = ref(false)
const confirmingDeletion = ref(false); const confirmingDeletion = ref(false)
onMounted(() => { onMounted(() => {
fetch(apiHost + 'games/' + gameId.value, { fetch(apiHost + 'games/' + gameId.value, {
method: 'GET', method: 'GET',
headers: { 'API-Version': 1 }, headers: { 'API-Version': 1 }
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((result) => { .then((result) => {
game.value = result; game.value = result
}) })
.catch((error) => { .catch((error) => {
console.error('Error:', error); console.error('Error:', error)
errorStore.unshift(error); errorStore.unshift(error)
}); })
}); })
function tryDeleteGame() { function tryDeleteGame() {
console.log("Try") console.log('Try')
confirmingDeletion.value = true; confirmingDeletion.value = true
new Promise((_) => setTimeout(_, 1000)).then(() => { new Promise((_) => setTimeout(_, 1000)).then(() => {
confirmingDeletion.value = false; confirmingDeletion.value = false
}); })
} }
function deleteGame() { function deleteGame() {
console.log("Delete") console.log('Delete')
fetch(`${apiHost}games/${gameId.value}`, { fetch(`${apiHost}games/${gameId.value}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${auth.value.token}`, Authorization: `Bearer ${auth.value.token}`,
'API-Version': 1, 'API-Version': 1
}, }
}) })
.then((response) => { .then((response) => {
if (response.ok) { if (response.ok) {
alert("Deleted") alert('Deleted')
router.back() router.back()
} else {
alert('Error deleting game')
} }
else { return true
alert("Error deleting game")
}
return true;
}) })
.catch((error) => { .catch((error) => {
errorStore.unshift(error); errorStore.unshift(error)
}); })
} }
function toggleActivation(state) { function toggleActivation(state) {
isActivating.value = true; isActivating.value = true
fetch(`${apiHost}games/${gameId.value}/${state ? 'activate' : 'deactivate'}`, { fetch(`${apiHost}games/${gameId.value}/${state ? 'activate' : 'deactivate'}`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${auth.value.token}`, Authorization: `Bearer ${auth.value.token}`,
'API-Version': 1, 'API-Version': 1
}, }
}) })
.then((response) => { .then((response) => {
if (response.status !== 204) return response.json().then((errorBody) => { throw new Error(errorBody); }); if (response.status !== 204)
return true; return response.json().then((errorBody) => {
throw new Error(errorBody)
})
return true
}) })
.then(() => { .then(() => {
game.value = { ...game.value, active: state }; game.value = { ...game.value, active: state }
isActivating.value = false; isActivating.value = false
}) })
.catch((error) => { .catch((error) => {
errorStore.unshift(error); errorStore.unshift(error)
isActivating.value = false; isActivating.value = false
}); })
} }
</script> </script>
<template> <template>
<article> <article>
<loader v-if="game === undefined"></loader> <loader v-if="game === undefined"></loader>
<div v-else> <div v-else>
<img v-if="game.image" :src="'data:image/png;base64,'+game.image" alt="thumbnail"/> <img v-if="game.image" :src="'data:image/png;base64,' + game.image" alt="thumbnail" />
<h1>{{ game.game }}</h1> <h1>{{ game.game }}</h1>
<p>{{ game.description }}</p> <p>{{ game.description }}</p>
<loader :variant="2" v-if="isActivating"></loader> <loader :variant="2" v-if="isActivating"></loader>
<template v-else> <template v-else>
<button <button
:class="game.active ? 'bg-red-500 text-white hover:bg-red-700' : 'bg-green-500 text-white hover:bg-green-700'" :class="
game.active
? 'bg-red-500 text-white hover:bg-red-700'
: 'bg-green-500 text-white hover:bg-green-700'
"
class="font-bold py-2 px-4 my-2 rounded" class="font-bold py-2 px-4 my-2 rounded"
@click="toggleActivation(!game.active)" @click="toggleActivation(!game.active)"
:disabled="isActivating" :disabled="isActivating"
@ -111,10 +116,10 @@ function toggleActivation(state) {
</button> </button>
<button <button
class="font-bold py-2 px-4 m-2 rounded bg-red-500 text-white hover:bg-red-700" class="font-bold py-2 px-4 m-2 rounded bg-red-500 text-white hover:bg-red-700"
@click="confirmingDeletion ? deleteGame() : tryDeleteGame()" @click="confirmingDeletion ? deleteGame() : tryDeleteGame()"
> >
{{ confirmingDeletion ? "Sure?" : "Delete" }} {{ confirmingDeletion ? 'Sure?' : 'Delete' }}
</button> </button>
</template> </template>
</div> </div>
@ -122,11 +127,9 @@ function toggleActivation(state) {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
article { article {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
</style>
</style>

View File

@ -7,7 +7,7 @@ import Loader from '@/components/Loader.vue'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import { GameService } from '@/services/game.service' import { GameService } from '@/services/game.service'
import { DownloadIcon } from 'lucide-vue-next' import { DownloadIcon } from 'lucide-vue-next'
import router from '@/router'; import router from '@/router'
const errorStore = useErrorStore() const errorStore = useErrorStore()
const apiHost = import.meta.env.VITE_CONJUREOS_HOST const apiHost = import.meta.env.VITE_CONJUREOS_HOST
@ -16,11 +16,10 @@ const { list: gamelist } = storeToRefs(gamelistStore)
const gamesService = new GameService() const gamesService = new GameService()
onMounted(async () => { onMounted(async () => {
const games = await gamesService.getGames() const games = await gamesService.getGames().catch((error) => {
.catch((error) => { console.error('Error:', error)
console.error('Error:', error) errorStore.unshift(error)
errorStore.unshift(error) })
})
if (games) { if (games) {
gamelistStore.set(games) gamelistStore.set(games)
@ -44,7 +43,7 @@ function downloadAll() {
} }
function newGame() { function newGame() {
router.push("/upload") router.push('/upload')
} }
</script> </script>
@ -70,9 +69,21 @@ function newGame() {
</div> </div>
</header> </header>
<ul class="flex flex-col gap-4"> <ul class="flex flex-col gap-4">
<li v-for="item in gamelist" :key="item.id" class="game-item border border-gray-300 rounded-lg p-4 flex items-center gap-4 hover:border-2"> <li
<RouterLink :to="'/games/' + item.id" class="flex-grow flex items-center gap-4 p-2 rounded"> v-for="item in gamelist"
<img v-if="item.thumbnail" :src="'data:image/png;base64,' + item.thumbnail" alt="thumbnail" class="h-16" /> :key="item.id"
class="game-item border border-gray-300 rounded-lg p-4 flex items-center gap-4 hover:border-2"
>
<RouterLink
:to="'/games/' + item.id"
class="flex-grow flex items-center gap-4 p-2 rounded"
>
<img
v-if="item.thumbnail"
:src="'data:image/png;base64,' + item.thumbnail"
alt="thumbnail"
class="h-16"
/>
<div> <div>
<h2 class="font-bold">{{ item.game }}</h2> <h2 class="font-bold">{{ item.game }}</h2>
<p class="text-primary">{{ item.description }}</p> <p class="text-primary">{{ item.description }}</p>

View File

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import DateInput from '@/components/DateInput.vue' import DateInput from '@/components/DateInput.vue'
import { UploadGameDto } from '@/dtos/upload-game.dto' import { UploadGameDto } from '@/dtos/upload-game.dto'
@ -11,10 +10,8 @@ import { FileSaver } from '@/utils/file-saver'
import { GuidUtil } from '@/utils/uuid.util' import { GuidUtil } from '@/utils/uuid.util'
import { ConjUtil } from '@/utils/conj.util' import { ConjUtil } from '@/utils/conj.util'
let playerValue = ref('')
let versionValue = ref('')
let playerValue = ref("");
let versionValue = ref("")
let uploadForm = ref() let uploadForm = ref()
// onMounted(() => { // onMounted(() => {
@ -22,120 +19,114 @@ let uploadForm = ref()
// }); // });
function validatePlayersInput(): void { function validatePlayersInput(): void {
let value = playerValue.value.replace(/[^1-2]/g, ""); // Allow only digits and hyphen let value = playerValue.value.replace(/[^1-2]/g, '') // Allow only digits and hyphen
value = value.slice(0, 3); // Ensure max length of 3 value = value.slice(0, 3) // Ensure max length of 3
if (value.length === 2 && !value.includes("-")) { if (value.length === 2 && !value.includes('-')) {
value = value.charAt(0) + "-" + value.charAt(1); // Auto-insert hyphen value = value.charAt(0) + '-' + value.charAt(1) // Auto-insert hyphen
} }
if (value.length === 3) { if (value.length === 3) {
const min = +value.charAt(0) const min = +value.charAt(0)
let max = +value.charAt(2) let max = +value.charAt(2)
if (max < min) if (max < min) max = min
max = min
value = `${min}-${max}` value = `${min}-${max}`
} }
playerValue.value = value playerValue.value = value
} }
function validateVersionInput(): void { function validateVersionInput(): void {
versionValue.value = versionValue.value.replace(/[^0-9.]/g, ""); // Allow only digits and hyphen versionValue.value = versionValue.value.replace(/[^0-9.]/g, '') // Allow only digits and hyphen
} }
async function submitForm(): Promise<void> { async function submitForm(): Promise<void> {
console.log("Submitting...") console.log('Submitting...')
const formData = new FormData(uploadForm.value) const formData = new FormData(uploadForm.value)
const files = (document.getElementById("medias") as HTMLInputElement).files const files = (document.getElementById('medias') as HTMLInputElement).files
let thumbnail = null; let thumbnail = null
let tbnFile = null; let tbnFile = null
let image = null let image = null
let imageFile = null; let imageFile = null
if (!!files && files.length > 0) { if (!!files && files.length > 0) {
tbnFile = files[0]; tbnFile = files[0]
thumbnail = new Uint8Array(await tbnFile.arrayBuffer()); thumbnail = new Uint8Array(await tbnFile.arrayBuffer())
} }
if (!!files && files.length > 1) { if (!!files && files.length > 1) {
imageFile = files[1]; imageFile = files[1]
image = new Uint8Array(await imageFile.arrayBuffer()); image = new Uint8Array(await imageFile.arrayBuffer())
} }
const game = formData.get("game") as File; const game = formData.get('game') as File
const gameSplit = game.name.split("."); const gameSplit = game.name.split('.')
const ext = gameSplit[gameSplit.length - 1]; const ext = gameSplit[gameSplit.length - 1]
let gamePath = ""; let gamePath = ''
if (ext === "zip") { if (ext === 'zip') {
let fileTree = await ZipUtil.getZipFileTree(game) let fileTree = await ZipUtil.getZipFileTree(game)
fileTree = fileTree.filter(file => file.includes(".exe")) fileTree = fileTree.filter((file) => file.includes('.exe'))
const title = formData.get("title") as string; const title = formData.get('title') as string
const bestMatch = ConjUtil.findBestExecutable(title, fileTree); const bestMatch = ConjUtil.findBestExecutable(title, fileTree)
if (!bestMatch) throw new Error("No executable found in zip file"); if (!bestMatch) throw new Error('No executable found in zip file')
const folder = BlobUtil.getFileName(game).split(".")[0] const folder = BlobUtil.getFileName(game).split('.')[0]
gamePath = `${folder}\\${bestMatch}`; gamePath = `${folder}\\${bestMatch}`
} } else if (ext === 'exe') {
else if (ext === "exe") { gamePath = game.name
gamePath = game.name; } else {
} throw new Error('Unsupported file type')
else {
throw new Error("Unsupported file type");
} }
const id = GuidUtil.generateUUIDv4(); const id = GuidUtil.generateUUIDv4()
const dto = new ValidateMetadataDto(formData, thumbnail, image, gamePath, id); const dto = new ValidateMetadataDto(formData, thumbnail, image, gamePath, id)
const service = new GameService(); const service = new GameService()
const response = await service.metadata(dto); const response = await service.metadata(dto)
let metadataText = await response.text(); let metadataText = await response.text()
console.log(metadataText) console.log(metadataText)
const metadataObj: any = {}; const metadataObj: any = {}
let metadataLines = metadataText.split("\n"); let metadataLines = metadataText.split('\n')
metadataLines = metadataLines.slice(0, metadataLines.length - 1); metadataLines = metadataLines.slice(0, metadataLines.length - 1)
for (const line of metadataLines) { for (const line of metadataLines) {
const lineSplit = line.split(":"); const lineSplit = line.split(':')
metadataObj[lineSplit[0].trim()] = lineSplit[1].trim(); metadataObj[lineSplit[0].trim()] = lineSplit[1].trim()
} }
if (!!tbnFile) if (!!tbnFile) metadataObj['thumbnailPath'] = `medias\\${BlobUtil.getFileName(tbnFile)}`
metadataObj["thumbnailPath"] = `medias\\${BlobUtil.getFileName(tbnFile)}` if (!!imageFile) metadataObj['imagePath'] = `medias\\${BlobUtil.getFileName(imageFile)}`
if (!!imageFile)
metadataObj["imagePath"] = `medias\\${BlobUtil.getFileName(imageFile)}`
console.log(metadataObj) console.log(metadataObj)
metadataText = Object.keys(metadataObj).map(key => [key, metadataObj[key]].join(": ")).join("\n"); metadataText = Object.keys(metadataObj)
console.log(metadataText); .map((key) => [key, metadataObj[key]].join(': '))
const metadataBlob = new Blob([metadataText], { type: "text/plain" }); .join('\n')
console.log(metadataText)
const metadataBlob = new Blob([metadataText], { type: 'text/plain' })
const zipUtil = new ZipUtil()
const zipUtil = new ZipUtil();
if (!!files) { if (!!files) {
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file = files[i]; const file = files[i]
const fileName = BlobUtil.getFileName(file); const fileName = BlobUtil.getFileName(file)
await zipUtil.addFile(`medias/${fileName}`, file); await zipUtil.addFile(`medias/${fileName}`, file)
} }
} }
await zipUtil.addFile("metadata.txt", metadataBlob); await zipUtil.addFile('metadata.txt', metadataBlob)
await zipUtil.addFile(BlobUtil.getFileName(game), game); await zipUtil.addFile(BlobUtil.getFileName(game), game)
const zipBlob = zipUtil.getZipBlob(); const zipBlob = zipUtil.getZipBlob()
const zipFile = new File([zipBlob], `${id}.conj`) const zipFile = new File([zipBlob], `${id}.conj`)
FileSaver.saveFile(zipFile); FileSaver.saveFile(zipFile)
// TODO remove form data // TODO remove form data
const conjUploadForm = new FormData(); const conjUploadForm = new FormData()
conjUploadForm.append("file", zipFile); conjUploadForm.append('file', zipFile)
const uploadResponse = await service.upload(conjUploadForm) const uploadResponse = await service.upload(conjUploadForm)
//alert(await uploadResponse.text()) //alert(await uploadResponse.text())
} }
function testData(): void { function testData(): void {
(document.getElementById("title") as HTMLInputElement).value = "Game Test"; ;(document.getElementById('title') as HTMLInputElement).value = 'Game Test'
(document.getElementById("description") as HTMLInputElement).value = "Ceci est un test"; ;(document.getElementById('description') as HTMLInputElement).value = 'Ceci est un test'
(document.getElementById("genres") as HTMLInputElement).value = "Action, Adventure"; ;(document.getElementById('genres') as HTMLInputElement).value = 'Action, Adventure'
(document.getElementById("devs") as HTMLInputElement).value = "Jean,Yussef"; ;(document.getElementById('devs') as HTMLInputElement).value = 'Jean,Yussef'
(document.getElementById("repo") as HTMLInputElement).value = "https://repo.com"; ;(document.getElementById('repo') as HTMLInputElement).value = 'https://repo.com'
(document.getElementById("players") as HTMLInputElement).value = "1-2"; ;(document.getElementById('players') as HTMLInputElement).value = '1-2'
(document.getElementById("release") as HTMLInputElement).value = "01/01/2021"; ;(document.getElementById('release') as HTMLInputElement).value = '01/01/2021'
(document.getElementById("version") as HTMLInputElement).value = "1.1.1"; ;(document.getElementById('version') as HTMLInputElement).value = '1.1.1'
} }
</script> </script>
<template> <template>
@ -144,29 +135,57 @@ function testData(): void {
<div class="text-foreground w-full my-2 underline"> <div class="text-foreground w-full my-2 underline">
<RouterLink to="upload" class="hover:color-blue-300">Or upload a .conj</RouterLink> <RouterLink to="upload" class="hover:color-blue-300">Or upload a .conj</RouterLink>
</div> </div>
<form ref="uploadForm" enctype="multipart/form-data" class="flex flex-col gap-2" @submit.prevent="submitForm"> <form
ref="uploadForm"
enctype="multipart/form-data"
class="flex flex-col gap-2"
@submit.prevent="submitForm"
>
<label for="title">Title</label> <label for="title">Title</label>
<input name="title" id="title" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" /> <input
name="title"
id="title"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
/>
<label for="description">Description</label> <label for="description">Description</label>
<textarea name="description" id="description" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" /> <textarea
name="description"
id="description"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
/>
<label for="medias">Media</label> <label for="medias">Media</label>
<input type="file" name="medias" id="medias" accept="video/quicktime,image/png,image/jpeg,video/mp4" multiple /> <input
type="file"
name="medias"
id="medias"
accept="video/quicktime,image/png,image/jpeg,video/mp4"
multiple
/>
<label for="game">Game</label> <label for="game">Game</label>
<input type="file" name="game" id="game" accept="application/zip,application/exe"/> <input type="file" name="game" id="game" accept="application/zip,application/exe" />
<label for="genres">Genres</label> <label for="genres">Genres</label>
<input name="genres" id="genres" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" <input
name="genres"
id="genres"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
placeholder="Action, Adventure" placeholder="Action, Adventure"
/> />
<label for="devs">Devs</label> <label for="devs">Devs</label>
<input name="devs" id="devs" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" <input
name="devs"
id="devs"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
placeholder="Jean, Yussef" placeholder="Jean, Yussef"
/> />
<label for="repo">Public Repository</label> <label for="repo">Public Repository</label>
<input name="repo" id="repo" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" <input
name="repo"
id="repo"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
placeholder="https://github.com/..." placeholder="https://github.com/..."
/> />
<label for="players">Player Count</label> <label for="players">Player Count</label>
@ -180,7 +199,7 @@ function testData(): void {
@input="validatePlayersInput" @input="validatePlayersInput"
maxlength="3" maxlength="3"
/> />
<!-- <date-input></date-input>--> <!-- <date-input></date-input>-->
<label for="release">Release Date</label> <label for="release">Release Date</label>
<input <input
name="release" name="release"
@ -200,11 +219,7 @@ function testData(): void {
/> />
<label for="leaderboard">Leaderboard</label> <label for="leaderboard">Leaderboard</label>
<input <input name="leaderboard" id="leaderboard" type="checkbox" />
name="leaderboard"
id="leaderboard"
type="checkbox"
/>
<button <button
class="bg-transparent font-bold text-foreground py-2 px-4 my-2 border border-primary rounded hover:bg-primary" class="bg-transparent font-bold text-foreground py-2 px-4 my-2 border border-primary rounded hover:bg-primary"
@ -216,6 +231,4 @@ function testData(): void {
</article> </article>
</template> </template>
<style scoped> <style scoped></style>
</style>

View File

@ -16,7 +16,7 @@ export default {
submitForm() { submitForm() {
const form = this.$refs.uploadForm const form = this.$refs.uploadForm
const formData = new FormData(form) const formData = new FormData(form)
console.log("Upload " + JSON.stringify(authStr.getAuth())) console.log('Upload ' + JSON.stringify(authStr.getAuth()))
fetch(apiHost + 'games', { fetch(apiHost + 'games', {
method: 'POST', method: 'POST',
@ -46,7 +46,12 @@ export default {
<template> <template>
<article> <article>
<h1 class="text-foreground">Upload</h1> <h1 class="text-foreground">Upload</h1>
<form ref="uploadForm" enctype="multipart/form-data" class="flex flex-col gap-2" @submit.prevent="submitForm"> <form
ref="uploadForm"
enctype="multipart/form-data"
class="flex flex-col gap-2"
@submit.prevent="submitForm"
>
<!-- <div class="name-input-wrapper">--> <!-- <div class="name-input-wrapper">-->
<!-- <label for="name" class="block text-sm font-medium text-gray-700">Name:</label>--> <!-- <label for="name" class="block text-sm font-medium text-gray-700">Name:</label>-->
<!-- <input type="text" name="name" id="name" required v-model="textInput"--> <!-- <input type="text" name="name" id="name" required v-model="textInput"-->
@ -70,7 +75,9 @@ export default {
</div> </div>
<div class="text-foreground w-full my-2 underline"> <div class="text-foreground w-full my-2 underline">
<RouterLink to="manual-upload" class="hover:color-blue-300">Or manually enter your metadata</RouterLink> <RouterLink to="manual-upload" class="hover:color-blue-300"
>Or manually enter your metadata</RouterLink
>
</div> </div>
<button <button
@ -84,7 +91,6 @@ export default {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
form { form {
padding: 1rem 0; padding: 1rem 0;
} }

View File

@ -1,11 +1,7 @@
<script setup> <script setup></script>
</script>
<template> <template>
<h1>Please close this page.</h1> <h1>Please close this page.</h1>
</template> </template>
<style scoped> <style scoped></style>
</style>

View File

@ -1,49 +1,47 @@
<script setup> <script setup>
import {ref} from 'vue' import { ref } from 'vue'
import {useRoute} from 'vue-router'; import { useRoute } from 'vue-router'
import Loader from '@/components/Loader.vue'; import Loader from '@/components/Loader.vue'
import router from '@/router' import router from '@/router'
import {useErrorStore} from '@/stores/errors' import { useErrorStore } from '@/stores/errors'
const errorStore = useErrorStore() const errorStore = useErrorStore()
const apiHost = import.meta.env.VITE_CONJUREOS_HOST const apiHost = import.meta.env.VITE_CONJUREOS_HOST
const isLoginIn = ref(false) const isLoginIn = ref(false)
const route = useRoute(); const route = useRoute()
if (!route.query.token || !route.query.action) { if (!route.query.token || !route.query.action) {
router.push("/close") router.push('/close')
} }
const submit = (form) => { const submit = (form) => {
const formData = new FormData(form) const formData = new FormData(form)
formData.set('token', route.query.token.toString()) formData.set('token', route.query.token.toString())
isLoginIn.value = true isLoginIn.value = true
const myHeaders = new Headers(); const myHeaders = new Headers()
myHeaders.append("API-Version", 1); myHeaders.append('API-Version', 1)
fetch(apiHost + route.query.action, { fetch(apiHost + route.query.action, {
method: 'POST', method: 'POST',
body: formData, body: formData,
headers: myHeaders, headers: myHeaders
}) })
.then((response) => { .then((response) => {
isLoginIn.value = false isLoginIn.value = false
if (response.status !== 200) if (response.status !== 200)
return response.text().then(error => { return response.text().then((error) => {
throw new Error(error) throw new Error(error)
} })
) return response.text()
return response.text() })
}) .then((result) => {
.then((result) => { router.push('/close')
router.push('/close') })
}) .catch((error) => {
.catch((error) => { isLoginIn.value = false
isLoginIn.value = false errorStore.unshift(error)
errorStore.unshift(error) })
})
} }
</script> </script>
@ -51,29 +49,29 @@ const submit = (form) => {
<form ref="loginForm" enctype="multipart/form-data" @submit.prevent="submit($refs.loginForm)"> <form ref="loginForm" enctype="multipart/form-data" @submit.prevent="submit($refs.loginForm)">
<label for="username">username</label> <label for="username">username</label>
<input <input
required required
type="text" type="text"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
name="username" name="username"
id="username" id="username"
/> />
<label for="password">password</label> <label for="password">password</label>
<input <input
required required
type="password" type="password"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
name="password" name="password"
id="password" id="password"
/> />
<button <button
v-if="!isLoginIn" v-if="!isLoginIn"
class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded" class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded"
type="submit" type="submit"
> >
Login Login
</button> </button>
<span class="loader" v-else> <span class="loader" v-else>
<Loader :variant="2"/> <Loader :variant="2" />
</span> </span>
</form> </form>
</template> </template>

View File

@ -0,0 +1,49 @@
<template>
<div>
<h1 class="text-2xl font-bold">Users Management</h1>
<loader v-if="users === undefined"></loader>
<div class="mt-4" v-else>
<table class="min-w-full border-gray-200">
<thead>
<tr>
<th class="py-2 px-4 border-b text-left">Email</th>
<th class="py-2 px-4 border-b text-left">Role</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td class="py-2 px-4 border-b">{{ user.email }}</td>
<td class="py-2 px-4 border-b">{{ user.role.label }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useErrorStore } from '@/stores/errors';
import { UserService } from '@/services/user.service';
import { useUsersStore } from '@/stores/users';
import { storeToRefs } from 'pinia';
import Loader from '@/components/Loader.vue'
const errorStore = useErrorStore()
const apiHost = import.meta.env.VITE_CONJUREOS_HOST
const usersStore = useUsersStore()
const { list: users } = storeToRefs(usersStore)
const userService = new UserService()
onMounted(async () => {
const users = await userService.getUsers().catch((error) => {
console.error('Error:', error)
errorStore.unshift(error)
})
if (users) {
usersStore.set(users)
}
})
</script>