Compare commits

..

No commits in common. "6a994cee31953a72cd6200d4da58856f9835dcca" and "174e7e57eb36f1b2d20c6e7adf1edeedac6cf091" have entirely different histories.

55 changed files with 1167 additions and 1334 deletions

View File

@ -1,39 +1,75 @@
# Stage 1: Build the application # syntax=docker/dockerfile:1
FROM node:20-alpine AS builder
WORKDIR /app # Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/engine/reference/builder/
# Copy package files and install dependencies ARG NODE_VERSION=20.8.1
COPY package.json package-lock.json ./
RUN npm ci
# Copy the rest of the application source code ################################################################################
# Use node image for base image for all stages.
FROM node:${NODE_VERSION}-alpine as base
# Set working directory for all build stages.
WORKDIR /usr/src/app
################################################################################
# Create a stage for installing production dependecies.
FROM base as deps
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.npm to speed up subsequent builds.
# Leverage bind mounts to package.json and package-lock.json to avoid having to copy them
# into this layer.
RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci --omit=dev
################################################################################
# Create a stage for building the application.
FROM deps as build
# Download additional development dependencies before building, as some projects require
# "devDependencies" to be installed to build. If you don't need this, remove this step.
RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci
# Copy the rest of the source files into the image.
COPY . . COPY . .
# Run the build script.
# Build the application for production
RUN npm run build RUN npm run build
# Stage 2: Serve the application with a simple Node.js server ################################################################################
FROM node:20-alpine # Create a new stage to run the application with minimal runtime dependencies
# where the necessary files are copied from the build stage.
FROM base as final
WORKDIR /app # Install http-server globally.
# Install a simple and lightweight HTTP server globally
RUN npm install -g http-server RUN npm install -g http-server
RUN npm install -g vite
# Copy the built static assets from the builder stage # Use production node environment by default.
COPY --from=builder /app/dist ./dist ENV NODE_ENV production
ENV VITE_CONJUREOS_HOST http://142.137.247.118:8080/
# Copy the custom entrypoint script to the image and make it executable # Run the application as a non-root user.
COPY entrypoint.sh /docker-entrypoint.sh USER node
RUN chmod +x /docker-entrypoint.sh
# Expose the port the server will run on, matching your compose file # Copy package.json so that package manager commands can be used.
COPY package.json .
COPY .env.production.local .env
# Copy the production dependencies from the deps stage and also
# the built application from the build stage into the image.
COPY --from=deps /usr/src/app/node_modules ./node_modules
COPY --from=build /usr/src/app/dist ./dist
# Expose the port that the application listens on.
EXPOSE 5174 EXPOSE 5174
# The entrypoint will run at container startup, performing any necessary setup. # Run the application.
# It should end with 'exec "$@"' to run the CMD. CMD http-server dist -p 5174
ENTRYPOINT ["/docker-entrypoint.sh"]
# The CMD specifies the main process to run: the http-server.
CMD ["http-server", "dist", "-p", "5174"]

View File

@ -13,7 +13,7 @@ services:
context: . context: .
environment: environment:
NODE_ENV: production NODE_ENV: production
VITE_CONJUREOS_HOST: http://host.docker.internal:8080/ VITE_CONJUREOS_HOST: http://142.137.247.118:8080/
ports: ports:
- 5174:5174 - 5174:5174

View File

@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
ROOT_DIR=/app/dist ROOT_DIR=/usr/share/nginx/html
echo "Replacing env constants in JS" echo "Replacing env constants in JS"
for file in $ROOT_DIR/js/app.*.js* $ROOT_DIR/index.html $ROOT_DIR/precache-manifest*.js; for file in $ROOT_DIR/js/app.*.js* $ROOT_DIR/index.html $ROOT_DIR/precache-manifest*.js;
@ -10,5 +10,3 @@ do
sed -i 's|VITE_CONJUREOS_HOST|'${VITE_CONJUREOS_HOST}'|g' $file sed -i 's|VITE_CONJUREOS_HOST|'${VITE_CONJUREOS_HOST}'|g' $file
done done
exec "$@"

20
eslint.config.cjs Normal file
View File

@ -0,0 +1,20 @@
/* 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'
]
}

View File

@ -1,44 +0,0 @@
// @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'
],
}
);

511
package-lock.json generated
View File

@ -40,7 +40,6 @@
"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"
@ -88,9 +87,12 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.28.6", "version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@ -505,9 +507,9 @@
} }
}, },
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.9.1", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
@ -523,21 +525,21 @@
} }
}, },
"node_modules/@eslint-community/regexpp": { "node_modules/@eslint-community/regexpp": {
"version": "4.12.2", "version": "4.12.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
"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"
} }
}, },
"node_modules/@eslint/config-array": { "node_modules/@eslint/config-array": {
"version": "0.21.1", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint/object-schema": "^2.1.7", "@eslint/object-schema": "^2.1.6",
"debug": "^4.3.1", "debug": "^4.3.1",
"minimatch": "^3.1.2" "minimatch": "^3.1.2"
}, },
@ -545,22 +547,10 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/config-helpers": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
"dev": true,
"dependencies": {
"@eslint/core": "^0.17.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": { "node_modules/@eslint/core": {
"version": "0.17.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz",
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.15" "@types/json-schema": "^7.0.15"
@ -570,9 +560,9 @@
} }
}, },
"node_modules/@eslint/eslintrc": { "node_modules/@eslint/eslintrc": {
"version": "3.3.3", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"ajv": "^6.12.4", "ajv": "^6.12.4",
@ -581,7 +571,7 @@
"globals": "^14.0.0", "globals": "^14.0.0",
"ignore": "^5.2.0", "ignore": "^5.2.0",
"import-fresh": "^3.2.1", "import-fresh": "^3.2.1",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.0",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1" "strip-json-comments": "^3.1.1"
}, },
@ -593,9 +583,9 @@
} }
}, },
"node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": {
"version": "4.2.1", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"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"
@ -605,14 +595,14 @@
} }
}, },
"node_modules/@eslint/eslintrc/node_modules/espree": { "node_modules/@eslint/eslintrc/node_modules/espree": {
"version": "10.4.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"acorn": "^8.15.0", "acorn": "^8.14.0",
"acorn-jsx": "^5.3.2", "acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -634,39 +624,48 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.39.2", "version": "9.20.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz",
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==",
"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"
},
"funding": {
"url": "https://eslint.org/donate"
} }
}, },
"node_modules/@eslint/object-schema": { "node_modules/@eslint/object-schema": {
"version": "2.1.7", "version": "2.1.6",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
"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"
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.4.1", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz",
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint/core": "^0.17.0", "@eslint/core": "^0.10.0",
"levn": "^0.4.1" "levn": "^0.4.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz",
"integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -716,9 +715,9 @@
} }
}, },
"node_modules/@humanwhocodes/retry": { "node_modules/@humanwhocodes/retry": {
"version": "0.4.3", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=18.18" "node": ">=18.18"
@ -1708,19 +1707,20 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.55.0", "version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz",
"integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", "integrity": "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.12.2", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/scope-manager": "8.24.1",
"@typescript-eslint/type-utils": "8.55.0", "@typescript-eslint/type-utils": "8.24.1",
"@typescript-eslint/utils": "8.55.0", "@typescript-eslint/utils": "8.24.1",
"@typescript-eslint/visitor-keys": "8.55.0", "@typescript-eslint/visitor-keys": "8.24.1",
"ignore": "^7.0.5", "graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"ts-api-utils": "^2.4.0" "ts-api-utils": "^2.0.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1730,31 +1730,22 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.55.0", "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <5.8.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.55.0", "version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz",
"integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/scope-manager": "8.24.1",
"@typescript-eslint/types": "8.55.0", "@typescript-eslint/types": "8.24.1",
"@typescript-eslint/typescript-estree": "8.55.0", "@typescript-eslint/typescript-estree": "8.24.1",
"@typescript-eslint/visitor-keys": "8.55.0", "@typescript-eslint/visitor-keys": "8.24.1",
"debug": "^4.4.3" "debug": "^4.3.4"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1765,38 +1756,17 @@
}, },
"peerDependencies": { "peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <5.8.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.55.0", "version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz",
"integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", "integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.55.0", "@typescript-eslint/types": "8.24.1",
"@typescript-eslint/visitor-keys": "8.55.0" "@typescript-eslint/visitor-keys": "8.24.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1806,33 +1776,16 @@
"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.55.0", "version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.1.tgz",
"integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", "integrity": "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.55.0", "@typescript-eslint/typescript-estree": "8.24.1",
"@typescript-eslint/typescript-estree": "8.55.0", "@typescript-eslint/utils": "8.24.1",
"@typescript-eslint/utils": "8.55.0", "debug": "^4.3.4",
"debug": "^4.4.3", "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"
@ -1843,13 +1796,13 @@
}, },
"peerDependencies": { "peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <5.8.0"
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.55.0", "version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz",
"integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==",
"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"
@ -1860,20 +1813,19 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.55.0", "version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz",
"integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.55.0", "@typescript-eslint/types": "8.24.1",
"@typescript-eslint/tsconfig-utils": "8.55.0", "@typescript-eslint/visitor-keys": "8.24.1",
"@typescript-eslint/types": "8.55.0", "debug": "^4.3.4",
"@typescript-eslint/visitor-keys": "8.55.0", "fast-glob": "^3.3.2",
"debug": "^4.4.3", "is-glob": "^4.0.3",
"minimatch": "^9.0.5", "minimatch": "^9.0.4",
"semver": "^7.7.3", "semver": "^7.6.0",
"tinyglobby": "^0.2.15", "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"
@ -1883,13 +1835,13 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <5.8.0"
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@ -1911,15 +1863,15 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.55.0", "version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz",
"integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", "integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.9.1", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/scope-manager": "8.24.1",
"@typescript-eslint/types": "8.55.0", "@typescript-eslint/types": "8.24.1",
"@typescript-eslint/typescript-estree": "8.55.0" "@typescript-eslint/typescript-estree": "8.24.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1930,17 +1882,17 @@
}, },
"peerDependencies": { "peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <5.8.0"
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.55.0", "version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz",
"integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.55.0", "@typescript-eslint/types": "8.24.1",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1951,9 +1903,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.1", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"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"
@ -2271,9 +2223,9 @@
} }
}, },
"node_modules/@vue/language-core/node_modules/brace-expansion": { "node_modules/@vue/language-core/node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@ -2389,9 +2341,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.15.0", "version": "8.14.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true, "dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -2598,9 +2550,9 @@
"dev": true "dev": true
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
@ -2940,9 +2892,9 @@
"dev": true "dev": true
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
}, },
@ -3082,9 +3034,9 @@
} }
}, },
"node_modules/editorconfig/node_modules/brace-expansion": { "node_modules/editorconfig/node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@ -3250,31 +3202,31 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.39.2", "version": "9.20.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.1", "@eslint/config-array": "^0.19.0",
"@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.11.0",
"@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.2.0",
"@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.20.0",
"@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.2.5",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2", "@humanwhocodes/retry": "^0.4.1",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4", "ajv": "^6.12.4",
"chalk": "^4.0.0", "chalk": "^4.0.0",
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
"debug": "^4.3.2", "debug": "^4.3.2",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.4.0", "eslint-scope": "^8.2.0",
"eslint-visitor-keys": "^4.2.1", "eslint-visitor-keys": "^4.2.0",
"espree": "^10.4.0", "espree": "^10.3.0",
"esquery": "^1.5.0", "esquery": "^1.5.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
@ -3419,9 +3371,9 @@
} }
}, },
"node_modules/eslint-plugin-import-x/node_modules/brace-expansion": { "node_modules/eslint-plugin-import-x/node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@ -3468,9 +3420,9 @@
} }
}, },
"node_modules/eslint-plugin-n/node_modules/brace-expansion": { "node_modules/eslint-plugin-n/node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@ -3615,9 +3567,9 @@
} }
}, },
"node_modules/eslint/node_modules/eslint-scope": { "node_modules/eslint/node_modules/eslint-scope": {
"version": "8.4.0", "version": "8.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz",
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esrecurse": "^4.3.0", "esrecurse": "^4.3.0",
@ -3631,9 +3583,9 @@
} }
}, },
"node_modules/eslint/node_modules/eslint-visitor-keys": { "node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.2.1", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
"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"
@ -3643,14 +3595,14 @@
} }
}, },
"node_modules/eslint/node_modules/espree": { "node_modules/eslint/node_modules/espree": {
"version": "10.4.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"acorn": "^8.15.0", "acorn": "^8.14.0",
"acorn-jsx": "^5.3.2", "acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -3892,15 +3844,14 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.5", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0", "es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
@ -3991,10 +3942,9 @@
} }
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "10.5.0", "version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dependencies": { "dependencies": {
"foreground-child": "^3.1.0", "foreground-child": "^3.1.0",
"jackspeak": "^3.1.2", "jackspeak": "^3.1.2",
@ -4022,9 +3972,9 @@
} }
}, },
"node_modules/glob/node_modules/brace-expansion": { "node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
@ -4076,6 +4026,12 @@
"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",
@ -4391,9 +4347,9 @@
} }
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.1", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@ -4516,9 +4472,9 @@
} }
}, },
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.23", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true "dev": true
}, },
"node_modules/lodash.castarray": { "node_modules/lodash.castarray": {
@ -5315,6 +5271,11 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/reinterval": { "node_modules/reinterval": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz",
@ -5486,9 +5447,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.4", "version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"dev": true, "dev": true,
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@ -5896,51 +5857,6 @@
"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",
@ -6007,9 +5923,9 @@
} }
}, },
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.4.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz",
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=18.12" "node": ">=18.12"
@ -6071,15 +5987,14 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.55.0", "version": "8.24.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.0.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.1.tgz",
"integrity": "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==", "integrity": "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.55.0", "@typescript-eslint/eslint-plugin": "8.24.1",
"@typescript-eslint/parser": "8.55.0", "@typescript-eslint/parser": "8.24.1",
"@typescript-eslint/typescript-estree": "8.55.0", "@typescript-eslint/utils": "8.24.1"
"@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"
@ -6090,7 +6005,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0", "eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0" "typescript": ">=4.8.4 <5.8.0"
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
@ -6171,9 +6086,9 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.21", "version": "5.4.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",

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 . --fix", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
@ -43,7 +43,6 @@
"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,6 +23,7 @@
--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 */

View File

@ -1,34 +1,47 @@
@import 'base.css'; @import 'base.css';
a.router-link-active {
color: var(--vt-c-munsell) !important; #app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
gap: 1rem;
font-weight: normal;
} }
a.router-link-active {
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,7 +18,10 @@
@input="updateDate" @input="updateDate"
></v-text-field> ></v-text-field>
</template> </template>
<v-date-picker :model-value="getDate" @update:modelValue="updateDate"></v-date-picker> <v-date-picker
:model-value="getDate"
@update:modelValue="updateDate"
></v-date-picker>
</v-menu> </v-menu>
</div> </div>
</template> </template>
@ -33,18 +36,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()
@ -56,16 +59,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>

View File

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

View File

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

View File

@ -1,51 +0,0 @@
<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,82 +9,83 @@ import SupportIcon from './icons/IconSupport.vue'
<template> <template>
<article> <article>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<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. <DocumentationIcon />
</WelcomeItem> </template>
<template #heading>Documentation</template>
<WelcomeItem> Vues
<template #icon> <a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
<ToolingIcon /> provides you with all information you need to get started.
</template> </WelcomeItem>
<template #heading>Tooling</template>
This project is served and bundled with <WelcomeItem>
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The <template #icon>
recommended IDE setup is <ToolingIcon />
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> + </template>
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If <template #heading>Tooling</template>
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 /> This project is served and bundled with
<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
>.
More instructions are available in <code>README.md</code>. <br />
</WelcomeItem>
<WelcomeItem> More instructions are available in <code>README.md</code>.
<template #icon> </WelcomeItem>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
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>, <EcosystemIcon />
<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>. <template #heading>Ecosystem</template>
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>
<WelcomeItem> Get official tools and libraries for your project:
<template #icon> <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<CommunityIcon /> <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>Community</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>
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 <CommunityIcon />
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener" </template>
>StackOverflow</a <template #heading>Community</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> Got stuck? Ask your question on
<template #icon> <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
<SupportIcon /> Discord server, or
</template> <a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
<template #heading>Support Vue</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>
As an independent project, Vue relies on community backing for its sustainability. You can <WelcomeItem>
help us by <template #icon>
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>. <SupportIcon />
</WelcomeItem> </template>
<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

@ -0,0 +1,11 @@
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 username: string;
password: string password: string;
public constructor(form: FormData) { public constructor(form: FormData) {
this.email = form.get('email') as string this.username = form.get("username") as string;
this.password = form.get('password') as string this.password = form.get("password") as string;
} }
} }

View File

@ -1,48 +1,50 @@
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,43 +1,39 @@
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(
formData: FormData, constructor(formData: FormData, thumbnail: Uint8Array | null, image: Uint8Array | null, filePath: string, id: string) {
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,11 +1,6 @@
export interface Role {
id: number
label: string
}
export interface AuthResponse { export interface AuthResponse {
id: number id: number;
role: Role roles: string[];
token: string token: string;
email: string username: 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
} }

View File

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

View File

@ -9,13 +9,14 @@ 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,6 +3,7 @@ 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: [
{ {
@ -25,28 +26,27 @@ 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,27 +96,10 @@ const router = createRouter({
}) })
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const authStore = useAuthStore() if (to.meta.requiresAuth === undefined)
const isLoggedIn = authStore.isAuth()
const userRole = authStore.getAuth()?.role?.label
// 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() next()
return
}
if (isLoggedIn === to.meta.requiresAuth) { if (useAuthStore().isAuth() === 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,11 +2,12 @@ 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,32 +1,28 @@
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 ( return await (await fetch(this.apiUrl + path, {
await fetch(this.apiUrl + path, { method: 'GET',
method: 'GET', headers: {
headers: { ...headers,
...headers, ...this.baseHeaders
...this.baseHeaders }
} })).json() as T
})
).json()) as T
} }
protected async post<T extends object>( protected async post<T extends object>(path: string, body: T, headers?: HeadersInit): Promise<Response> {
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,
@ -35,7 +31,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> {
@ -46,7 +42,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> {
@ -58,7 +54,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> {
@ -68,6 +64,6 @@ export class BaseService {
...headers, ...headers,
...this.baseHeaders ...this.baseHeaders
} }
}) });
} }
} }

View File

@ -5,22 +5,23 @@ 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)
} }

View File

@ -1,12 +0,0 @@
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,13 +4,17 @@ 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) localStorage.setItem(key, JSON.stringify(_auth)) if (_auth)
else localStorage.removeItem(key) localStorage.setItem(key, JSON.stringify(_auth))
else
localStorage.removeItem(key)
} }
function isAuth() { function isAuth() {

View File

@ -1,18 +1,20 @@
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 getErrors() { function unshift(error: string) {
return errors console.error('Error:', error);
} errors.value.unshift(error)
}
return { errors, getErrors, unshift }
function getErrors() {
return errors
}
return { errors, getErrors, unshift}
}) })

View File

@ -1,25 +1,28 @@
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)
}
function isAuth() { /** @param {(undefined | string)} auth */
return !!this.auth function set(auth) {
} this.auth = auth
if (auth)
localStorage.setItem(key, auth.toString())
else
localStorage.removeItem(key)
}
function getAuth() { function isAuth() {
return this.auth return !!this.auth
} }
return { auth, getAuth, set, isAuth } function getAuth() {
return this.auth
}
return {auth, getAuth, set, isAuth}
}) })

View File

@ -1,13 +0,0 @@
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,7 +1,7 @@
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,51 +1,46 @@
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', "unitycrashhandler", "unrealengine", "unrealeditor",
'unrealengine', "launcher", "updater", "configtool", "settings"
'unrealeditor', ];
'launcher', if (engineFiles.some(engine => normalizedFile.includes(engine))) score -= 50;
'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,163 +1,147 @@
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( private createCentralDirectoryEntry(fileName: string, fileSize: number, crc: number, fileOffset: number): Uint8Array<ArrayBuffer> {
fileName: string, const encodedFileName = new TextEncoder().encode(fileName);
fileSize: number, const entry = new Uint8Array(46 + encodedFileName.length);
crc: number, const view = new DataView(entry.buffer);
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( view.setUint32(12, this.centralDirectory.reduce((sum, entry) => sum + entry.length, 0), true); // Size of central directory
12, view.setUint32(16, this.offset, true); // Offset of central directory
this.centralDirectory.reduce((sum, entry) => sum + entry.length, 0), view.setUint16(20, 0, true); // Comment length
true return eocd;
) // 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.centralDirectory.push(this.createCentralDirectoryEntry(filePath, fileData.byteLength, this.crc32(new Uint8Array(fileData)), fileOffset));
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,26 +8,18 @@ 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 <img alt="Conjure logo" class="logo" src="@/assets/logo_conjure_dark.png" width="2228" height="349" />
alt="Conjure logo"
class="logo"
src="@/assets/logo_conjure_dark.png"
width="2228"
height="349"
/>
</header> </header>
<RouterView /> <RouterView />
<footer> <footer>
<router-link <router-link v-if="auth" to="/"
v-if="auth" 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="/"
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 />
@ -35,6 +27,7 @@ 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,25 +1,114 @@
<script setup> <script setup>
import { RouterView } from 'vue-router' import {RouterLink, RouterView } from 'vue-router'
import Sidebar from '@/components/Sidebar.vue'
import Header from '@/components/Header.vue' import {storeToRefs} from 'pinia';
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>
<div class="flex h-screen w-screen bg-muted/40"> <header class="flex flex-row justify-between items-center mb-8">
<Sidebar /> <div class="flex flex-row items-center mb-8 gap-10">
<div class="flex flex-col flex-1"> <RouterLink to="/">
<Header /> <img alt="Conjure logo" class="logo max-w-lg" src="@/assets/logo_conjure_dark.png"/>
<main class="flex-1 p-8 overflow-y-auto"> </RouterLink>
<RouterView /> <!-- <nav>-->
</main> <!-- <RouterLink to="/games">Games</RouterLink>-->
<footer class="p-4"> <!-- <RouterLink to="/upload">Upload</RouterLink>-->
<Errors /> <!-- </nav>-->
</footer>
</div> </div>
</div> <button id="logout" class="border-transparent font-bold text-foreground py-2 px-4 border hover:border-primary rounded mb-5"
@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,16 +1,19 @@
<script setup> <script setup>
import { RouterLink, useRoute, useRouter } from 'vue-router'
const route = useRoute() import {RouterLink, useRoute, useRouter} from 'vue-router';
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
@ -20,22 +23,24 @@ function getPathSegment() {
} }
return back return back
} }
</script> </script>
<template> <template>
<article> <article>
<h1>Shit not found bruh</h1> <h1>
<RouterLink Shit not found bruh
:to="getPathSegment()" </h1>
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" <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">
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;
@ -59,4 +64,5 @@ ul {
} }
} }
} }
</style> </style>

View File

@ -1,4 +1,5 @@
<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>
@ -8,13 +9,7 @@ import Errors from '@/components/Errors.vue'
<div id="auth-content"> <div id="auth-content">
<header> <header>
<RouterLink to="/"> <RouterLink to="/">
<img <img alt="Conjure logo" class="logo" src="@/assets/logo_conjure_dark.png" width="2228" height="349"/>
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>
@ -22,24 +17,15 @@ 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;

View File

@ -1,4 +1,5 @@
<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'
@ -8,24 +9,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('/')
} }
@ -34,18 +35,14 @@ const login = async (form: HTMLFormElement) => {
<template> <template>
<article> <article>
<h1>Login</h1> <h1>Login</h1>
<form <form ref="loginForm" enctype="multipart/form-data" @submit.prevent="login($refs.loginForm as HTMLFormElement)">
ref="loginForm" <label for="username">Username</label>
enctype="multipart/form-data"
@submit.prevent="login($refs.loginForm as HTMLFormElement)"
>
<label for="email">Email</label>
<input <input
required required
type="email" 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="email" name="username"
id="email" id="username"
/> />
<label for="password">Password</label> <label for="password">Password</label>
<input <input

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,9 +21,10 @@ 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) => {
@ -37,36 +38,24 @@ 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 <form ref="signupForm" enctype="multipart/form-data" @submit.prevent="signup($refs.signupForm as HTMLFormElement)">
ref="signupForm" <label for="username">Username</label>
enctype="multipart/form-data" <input required type="text"
@submit.prevent="signup($refs.signupForm as HTMLFormElement)" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
> name="username" id="username" />
<label for="email">Email</label>
<input
required
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 <input required type="password"
required class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
type="password" name="password" id="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>

View File

@ -1,113 +1,108 @@
<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')
} }
return true else {
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) if (response.status !== 204) return response.json().then((errorBody) => { throw new Error(errorBody); });
return response.json().then((errorBody) => { return true;
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=" :class="game.active ? 'bg-red-500 text-white hover:bg-red-700' : 'bg-green-500 text-white hover:bg-green-700'"
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"
@ -116,10 +111,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>
@ -127,9 +122,11 @@ 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,10 +16,11 @@ const { list: gamelist } = storeToRefs(gamelistStore)
const gamesService = new GameService() const gamesService = new GameService()
onMounted(async () => { onMounted(async () => {
const games = await gamesService.getGames().catch((error) => { const games = await gamesService.getGames()
console.error('Error:', error) .catch((error) => {
errorStore.unshift(error) console.error('Error:', error)
}) errorStore.unshift(error)
})
if (games) { if (games) {
gamelistStore.set(games) gamelistStore.set(games)
@ -43,7 +44,7 @@ function downloadAll() {
} }
function newGame() { function newGame() {
router.push('/upload') router.push("/upload")
} }
</script> </script>
@ -69,21 +70,9 @@ function newGame() {
</div> </div>
</header> </header>
<ul class="flex flex-col gap-4"> <ul class="flex flex-col gap-4">
<li <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">
v-for="item in gamelist" <RouterLink :to="'/games/' + item.id" class="flex-grow flex items-center gap-4 p-2 rounded">
:key="item.id" <img v-if="item.thumbnail" :src="'data:image/png;base64,' + item.thumbnail" alt="thumbnail" class="h-16" />
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,4 +1,5 @@
<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'
@ -10,8 +11,10 @@ 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(() => {
@ -19,114 +22,120 @@ 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) max = min if (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') { }
gamePath = game.name else if (ext === "exe") {
} else { gamePath = game.name;
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) metadataObj['thumbnailPath'] = `medias\\${BlobUtil.getFileName(tbnFile)}` if (!!tbnFile)
if (!!imageFile) metadataObj['imagePath'] = `medias\\${BlobUtil.getFileName(imageFile)}` metadataObj["thumbnailPath"] = `medias\\${BlobUtil.getFileName(tbnFile)}`
if (!!imageFile)
metadataObj["imagePath"] = `medias\\${BlobUtil.getFileName(imageFile)}`
console.log(metadataObj) console.log(metadataObj)
metadataText = Object.keys(metadataObj) metadataText = Object.keys(metadataObj).map(key => [key, metadataObj[key]].join(": ")).join("\n");
.map((key) => [key, metadataObj[key]].join(': ')) console.log(metadataText);
.join('\n') const metadataBlob = new Blob([metadataText], { type: "text/plain" });
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>
@ -135,57 +144,29 @@ 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 <form ref="uploadForm" enctype="multipart/form-data" class="flex flex-col gap-2" @submit.prevent="submitForm">
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 <input name="title" id="title" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" />
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 <textarea name="description" id="description" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" />
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 <input type="file" name="medias" id="medias" accept="video/quicktime,image/png,image/jpeg,video/mp4" multiple />
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 <input name="genres" id="genres" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
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 <input name="devs" id="devs" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
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 <input name="repo" id="repo" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
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>
@ -199,7 +180,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"
@ -219,7 +200,11 @@ function testData(): void {
/> />
<label for="leaderboard">Leaderboard</label> <label for="leaderboard">Leaderboard</label>
<input name="leaderboard" id="leaderboard" type="checkbox" /> <input
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"
@ -231,4 +216,6 @@ function testData(): void {
</article> </article>
</template> </template>
<style scoped></style> <style scoped>
</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,12 +46,7 @@ export default {
<template> <template>
<article> <article>
<h1 class="text-foreground">Upload</h1> <h1 class="text-foreground">Upload</h1>
<form <form ref="uploadForm" enctype="multipart/form-data" class="flex flex-col gap-2" @submit.prevent="submitForm">
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"-->
@ -75,9 +70,7 @@ 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" <RouterLink to="manual-upload" class="hover:color-blue-300">Or manually enter your metadata</RouterLink>
>Or manually enter your metadata</RouterLink
>
</div> </div>
<button <button
@ -91,6 +84,7 @@ export default {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
form { form {
padding: 1rem 0; padding: 1rem 0;
} }

45
src/views/mqtt/Events.vue Normal file
View File

@ -0,0 +1,45 @@
<script setup>
import {ref, onMounted, onUnmounted} from 'vue';
import mqtt from 'mqtt';
const wsHost = import.meta.env.VITE_CONJUREOS_MQTT
const topic = '#';
const receivedMessages = ref([]);
let client;
onMounted(() => {
// Connect to MQTT broker
client = mqtt.connect(wsHost, {clientId: 'frontend', protocol: 'ws'});
// Subscribe to a topic
client.subscribe(topic);
// Handle incoming messages
client.on('message', (topic, message) => {
receivedMessages.value.push({topic, message: JSON.parse(message.toString())});
});
// Additional setup or event listeners if needed
});
// Cleanup on component unmount
onUnmounted(() => {
// Unsubscribe and disconnect when the component is unmounted
if (client) {
client.unsubscribe(topic);
client.end();
}
});
</script>
<template>
<div>
<h1>Received Message:</h1>
<p v-for="receivedMessage in receivedMessages"> {{ receivedMessage.topic }} : {{receivedMessage.message["username"]}}</p>
<!-- Your template content goes here -->
</div>
</template>
<style scoped>
/* Your scoped styles go here */
</style>

View File

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

View File

@ -1,47 +1,49 @@
<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) => { })
router.push('/close') .then((result) => {
}) router.push('/close')
.catch((error) => { })
isLoginIn.value = false .catch((error) => {
errorStore.unshift(error) isLoginIn.value = false
}) errorStore.unshift(error)
})
} }
</script> </script>
@ -49,29 +51,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

@ -1,49 +0,0 @@
<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>

View File

@ -5,7 +5,7 @@
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node", "moduleResolution": "Node",
"strict": false, "strict": true,
"jsx": "preserve", "jsx": "preserve",
"sourceMap": true, "sourceMap": true,
"resolveJsonModule": true, "resolveJsonModule": true,