Compare commits
5 Commits
master
...
tristan-wi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a994cee31 | ||
|
|
fddcd1fecd | ||
|
|
174e7e57eb | ||
|
|
4a26259891 | ||
|
|
5fd32ef333 |
2
.env.sample
Normal file
2
.env.sample
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
VITE_CONJUREOS_HOST=
|
||||||
|
VITE_CONJUREOS_MQTT=
|
||||||
@ -1,14 +0,0 @@
|
|||||||
/* eslint-env node */
|
|
||||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
'extends': [
|
|
||||||
'plugin:vue/vue3-essential',
|
|
||||||
'eslint:recommended',
|
|
||||||
'@vue/eslint-config-prettier/skip-formatting'
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 'latest'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,3 +26,4 @@ coverage
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
.env
|
||||||
|
|||||||
88
Dockerfile
88
Dockerfile
@ -1,75 +1,39 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# Stage 1: Build the application
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
# Comments are provided throughout this file to help you get started.
|
WORKDIR /app
|
||||||
# If you need more help, visit the Dockerfile reference guide at
|
|
||||||
# https://docs.docker.com/engine/reference/builder/
|
|
||||||
|
|
||||||
ARG NODE_VERSION=20.8.1
|
# Copy package files and install dependencies
|
||||||
|
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
|
||||||
# Create a new stage to run the application with minimal runtime dependencies
|
FROM node:20-alpine
|
||||||
# where the necessary files are copied from the build stage.
|
|
||||||
FROM base as final
|
|
||||||
|
|
||||||
# Install http-server globally.
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 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
|
|
||||||
|
|
||||||
# Use production node environment by default.
|
# Copy the built static assets from the builder stage
|
||||||
ENV NODE_ENV production
|
COPY --from=builder /app/dist ./dist
|
||||||
ENV VITE_CONJUREOS_HOST http://142.137.247.118:8080/
|
|
||||||
|
|
||||||
# Run the application as a non-root user.
|
# Copy the custom entrypoint script to the image and make it executable
|
||||||
USER node
|
COPY entrypoint.sh /docker-entrypoint.sh
|
||||||
|
RUN chmod +x /docker-entrypoint.sh
|
||||||
|
|
||||||
# Copy package.json so that package manager commands can be used.
|
# Expose the port the server will run on, matching your compose file
|
||||||
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
|
||||||
|
|
||||||
# Run the application.
|
# The entrypoint will run at container startup, performing any necessary setup.
|
||||||
CMD http-server dist -p 5174
|
# It should end with 'exec "$@"' to run the CMD.
|
||||||
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
|
||||||
|
# The CMD specifies the main process to run: the http-server.
|
||||||
|
CMD ["http-server", "dist", "-p", "5174"]
|
||||||
|
|||||||
@ -13,7 +13,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
VITE_CONJUREOS_HOST: http://142.137.247.118:8080/
|
VITE_CONJUREOS_HOST: http://host.docker.internal:8080/
|
||||||
ports:
|
ports:
|
||||||
- 5174:5174
|
- 5174:5174
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
ROOT_DIR=/usr/share/nginx/html
|
ROOT_DIR=/app/dist
|
||||||
|
|
||||||
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,3 +10,5 @@ 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 "$@"
|
||||||
44
eslint.config.mjs
Normal file
44
eslint.config.mjs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import vue from 'eslint-plugin-vue';
|
||||||
|
import prettier from '@vue/eslint-config-prettier';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
...vue.configs['flat/vue3-essential'],
|
||||||
|
// This should be last to override other formatting rules
|
||||||
|
prettier,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
// Vue compiler macros
|
||||||
|
'defineProps': 'readonly',
|
||||||
|
'defineEmits': 'readonly',
|
||||||
|
'defineExpose': 'readonly',
|
||||||
|
'withDefaults': 'readonly'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Limit linting to specific file types
|
||||||
|
files: ['src/**/*.vue', 'src/**/*.js', 'src/**/*.ts'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Ignore files from linting
|
||||||
|
ignores: [
|
||||||
|
'node_modules',
|
||||||
|
'dist',
|
||||||
|
'src/**/*.d.ts',
|
||||||
|
'*.config.js',
|
||||||
|
'*.config.cjs'
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
@ -8,6 +8,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
8503
package-lock.json
generated
8503
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@ -4,37 +4,48 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
|
"lint": "eslint . --fix",
|
||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@date-io/date-fns": "^3.2.1",
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
"@tailwindcss/container-queries": "^0.1.1",
|
"@tailwindcss/container-queries": "^0.1.1",
|
||||||
"@tailwindcss/forms": "^0.5.6",
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
"lucide-vue-next": "^0.479.0",
|
||||||
"mqtt": "^5.3.3",
|
"mqtt": "^5.3.3",
|
||||||
"pinia": "^2.1.6",
|
"pinia": "^2.1.6",
|
||||||
|
"vite-plugin-vuetify": "^2.1.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.4"
|
"vue-router": "^4.2.4",
|
||||||
|
"vuetify": "^3.7.16"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.3.3",
|
"@rushstack/eslint-patch": "^1.10.5",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.24.1",
|
||||||
|
"@typescript-eslint/parser": "^8.24.1",
|
||||||
"@vitejs/plugin-vue": "^4.3.4",
|
"@vitejs/plugin-vue": "^4.3.4",
|
||||||
"@vue/eslint-config-prettier": "^8.0.0",
|
"@vue/eslint-config-prettier": "^8.0.0",
|
||||||
|
"@vue/eslint-config-standard": "^9.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^14.4.0",
|
||||||
"@vue/test-utils": "^2.4.1",
|
"@vue/test-utils": "^2.4.1",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.49.0",
|
"eslint": "^9.20.1",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^22.1.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.0.3",
|
||||||
"sass": "^1.68.0",
|
"sass": "^1.68.0",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"vite": "^4.4.9",
|
"typescript": "^5.7.3",
|
||||||
"vitest": "^0.34.4"
|
"typescript-eslint": "^8.55.0",
|
||||||
|
"vite": "^5.4.14",
|
||||||
|
"vitest": "^3.0.8",
|
||||||
|
"vue-tsc": "^2.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
<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()
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
/* color palette from <https://github.com/vuejs/theme> */
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--vt-c-white: #FDFDFF;
|
--vt-c-white: #fdfdff;
|
||||||
--vt-c-white-soft: #f8f8f8;
|
--vt-c-white-soft: #f8f8f8;
|
||||||
--vt-c-white-mute: #f2f2f2;
|
--vt-c-white-mute: #f2f2f2;
|
||||||
|
|
||||||
--vt-c-black: #171A1B;
|
--vt-c-black: #171a1b;
|
||||||
--vt-c-black-soft: #2C2F31;
|
--vt-c-black-soft: #2c2f31;
|
||||||
--vt-c-black-mute: #393D3F;
|
--vt-c-black-mute: #393d3f;
|
||||||
|
|
||||||
--vt-c-payne: 135, 151, 163;
|
--vt-c-payne: 135, 151, 163;
|
||||||
--vt-c-bittersweet: #C14953;
|
--vt-c-bittersweet: #c14953;
|
||||||
--vt-c-silver: #C6C5B9;
|
--vt-c-silver: #c6c5b9;
|
||||||
--vt-c-munsell: #8EC5CC;
|
--vt-c-munsell: #8ec5cc;
|
||||||
|
|
||||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||||
@ -23,7 +23,6 @@
|
|||||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||||
--vt-c-text-dark-1: var(--vt-c-white);
|
--vt-c-text-dark-1: var(--vt-c-white);
|
||||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* semantic color variables for this project */
|
/* semantic color variables for this project */
|
||||||
|
|||||||
@ -1,27 +1,15 @@
|
|||||||
@import 'base.css';
|
@import 'base.css';
|
||||||
|
|
||||||
|
|
||||||
#app {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
gap: 1rem;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
a.router-link-active {
|
a.router-link-active {
|
||||||
color: var(--vt-c-munsell) !important;
|
color: var(--vt-c-munsell) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--vt-c-silver);
|
color: var(--vt-c-silver);
|
||||||
transition: 0.4s;
|
transition: 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
/*a:hover,*/
|
/*a:hover,*/
|
||||||
/*button:hover {*/
|
/*button:hover {*/
|
||||||
@ -29,24 +17,10 @@ a {
|
|||||||
/*}*/
|
/*}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* 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 {
|
||||||
|
|||||||
71
src/components/DateInput.vue
Normal file
71
src/components/DateInput.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-menu
|
||||||
|
v-model="menu"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
:nudge-right="40"
|
||||||
|
transition="scale-transition"
|
||||||
|
offset-y
|
||||||
|
min-width="290px"
|
||||||
|
>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-text-field
|
||||||
|
v-bind="props"
|
||||||
|
:value="dateFormatted"
|
||||||
|
variant="outlined"
|
||||||
|
append-inner-icon="mdi-calendar"
|
||||||
|
@change="onChange"
|
||||||
|
@input="updateDate"
|
||||||
|
></v-text-field>
|
||||||
|
</template>
|
||||||
|
<v-date-picker :model-value="getDate" @update:modelValue="updateDate"></v-date-picker>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* Date on ISO format to be edited.
|
||||||
|
* @model
|
||||||
|
*/
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default() {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
menu: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dateFormatted() {
|
||||||
|
return this.input ? new Date(this.input) : ''
|
||||||
|
},
|
||||||
|
getDate() {
|
||||||
|
const date = this.input ? new Date(this.input) : new Date()
|
||||||
|
return [date]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onChange(val) {
|
||||||
|
console.log(val)
|
||||||
|
},
|
||||||
|
updateDate(val) {
|
||||||
|
this.menu = false
|
||||||
|
console.log(val)
|
||||||
|
this.input = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.v-text-field input {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
32
src/components/Header.vue
Normal file
32
src/components/Header.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<script setup>
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import router from '@/router'
|
||||||
|
import { LogOutIcon } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
authStore.set(null)
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<header class="flex h-16 items-center justify-between border-b px-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<RouterLink to="/">
|
||||||
|
<img alt="Conjure logo" class="h-8" src="@/assets/logo_conjure_dark.png" />
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
@click="logout"
|
||||||
|
class="flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<LogOutIcon class="h-5 w-5" />
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
@ -10,9 +10,7 @@ defineProps({
|
|||||||
<template>
|
<template>
|
||||||
<div class="greetings">
|
<div class="greetings">
|
||||||
<h1 class="green">{{ msg }}</h1>
|
<h1 class="green">{{ msg }}</h1>
|
||||||
<h3>
|
<h3>You’re successfully a foken 🍞 in the ass.</h3>
|
||||||
You’re successfully a foken 🍞 in the ass.
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@ -161,7 +161,6 @@ defineProps({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/components/Sidebar.vue
Normal file
51
src/components/Sidebar.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<aside :class="[' text-foreground p-4 transition-all duration-300 border-r', isCollapsed ? 'w-16' : 'w-64']">
|
||||||
|
<div class="flex items-center" :class="isCollapsed ? 'justify-center' : 'justify-end'">
|
||||||
|
<button @click="toggleCollapse" class="p-2 rounded-full hover:bg-muted">
|
||||||
|
<PanelLeft v-if="!isCollapsed" class="h-6 w-6" />
|
||||||
|
<PanelRight v-if="isCollapsed" class="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="mt-8">
|
||||||
|
<div v-if="isAdmin" class="rounded-lg hover:bg-muted transition-colors duration-200 p-2">
|
||||||
|
<RouterLink
|
||||||
|
to="/users"
|
||||||
|
class="flex items-center"
|
||||||
|
:title="isCollapsed ? 'Users' : ''"
|
||||||
|
>
|
||||||
|
<Users class="h-6 w-6" />
|
||||||
|
<span v-if="!isCollapsed" class="ml-4">Users</span>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<!-- Other links can go here -->
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { RouterLink } from 'vue-router';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { PanelLeft, PanelRight, Users } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
const isCollapsed = ref(false);
|
||||||
|
|
||||||
|
function toggleCollapse() {
|
||||||
|
isCollapsed.value = !isCollapsed.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const { auth } = storeToRefs(authStore);
|
||||||
|
|
||||||
|
const isAdmin = ref(auth.value?.role?.label === 'Admin');
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Add any specific styles for the sidebar here */
|
||||||
|
.router-link-exact-active {
|
||||||
|
background-color: hsl(var(--muted-foreground) / 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -9,7 +9,6 @@ import SupportIcon from './icons/IconSupport.vue'
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<article>
|
<article>
|
||||||
|
|
||||||
<WelcomeItem>
|
<WelcomeItem>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<DocumentationIcon />
|
<DocumentationIcon />
|
||||||
@ -53,8 +52,8 @@ import SupportIcon from './icons/IconSupport.vue'
|
|||||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>.
|
||||||
you need more resources, we suggest paying
|
If you need more resources, we suggest paying
|
||||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||||
a visit.
|
a visit.
|
||||||
</WelcomeItem>
|
</WelcomeItem>
|
||||||
@ -71,8 +70,8 @@ import SupportIcon from './icons/IconSupport.vue'
|
|||||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||||
>StackOverflow</a
|
>StackOverflow</a
|
||||||
>. You should also subscribe to
|
>. You should also subscribe to
|
||||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and
|
||||||
the official
|
follow the official
|
||||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||||
twitter account for latest news in the Vue world.
|
twitter account for latest news in the Vue world.
|
||||||
</WelcomeItem>
|
</WelcomeItem>
|
||||||
@ -83,8 +82,8 @@ import SupportIcon from './icons/IconSupport.vue'
|
|||||||
</template>
|
</template>
|
||||||
<template #heading>Support Vue</template>
|
<template #heading>Support Vue</template>
|
||||||
|
|
||||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
As an independent project, Vue relies on community backing for its sustainability. You can
|
||||||
us by
|
help us by
|
||||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||||
</WelcomeItem>
|
</WelcomeItem>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import HelloWorld from '../HelloWorld.vue'
|
|
||||||
|
|
||||||
describe('HelloWorld', () => {
|
|
||||||
it('renders properly', () => {
|
|
||||||
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
|
|
||||||
expect(wrapper.text()).toContain('Hello Vitest')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
9
src/dtos/auth.dto.ts
Normal file
9
src/dtos/auth.dto.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export class AuthDto {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
|
||||||
|
public constructor(form: FormData) {
|
||||||
|
this.email = form.get('email') as string
|
||||||
|
this.password = form.get('password') as string
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/dtos/upload-game.dto.ts
Normal file
48
src/dtos/upload-game.dto.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
export class UploadGameDto {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
medias: Blob[]
|
||||||
|
filesBlob: Blob
|
||||||
|
files?: string
|
||||||
|
genres: string[]
|
||||||
|
developers: string[]
|
||||||
|
publicRepositoryLink: string
|
||||||
|
players: string
|
||||||
|
release: string
|
||||||
|
modification: string
|
||||||
|
version: string
|
||||||
|
leaderboard: boolean
|
||||||
|
|
||||||
|
thumbnail?: string
|
||||||
|
image?: string
|
||||||
|
|
||||||
|
constructor(formData: FormData, mediaFileList: FileList) {
|
||||||
|
this.title = formData.get('title') as string
|
||||||
|
this.description = formData.get('description') as string
|
||||||
|
this.publicRepositoryLink = formData.get('repo') as string
|
||||||
|
this.players = formData.get('players') as string
|
||||||
|
this.release = new Date(formData.get('release') as string).toISOString().split('T')[0]
|
||||||
|
this.modification = new Date().toISOString().split('T')[0]
|
||||||
|
this.version = formData.get('version') as string
|
||||||
|
this.leaderboard = !!formData.get('leaderboard')
|
||||||
|
this.medias = []
|
||||||
|
for (let i = 0; i < mediaFileList.length; i++) {
|
||||||
|
this.medias[i] = mediaFileList[i]
|
||||||
|
}
|
||||||
|
this.filesBlob = formData.get('game') as Blob
|
||||||
|
this.genres = (formData.get('genres') as string).split(',').map((s) => s.trim())
|
||||||
|
this.developers = (formData.get('devs') as string).split(',').map((s) => s.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
async setMediasAndFiles() {
|
||||||
|
if (this.medias.length > 0) {
|
||||||
|
this.thumbnail = await this.medias[0].text()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.medias.length > 1) {
|
||||||
|
this.image = await this.medias[1].text()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.files = await this.filesBlob.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/dtos/validate-metadata.dto.ts
Normal file
43
src/dtos/validate-metadata.dto.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
export class ValidateMetadataDto {
|
||||||
|
id: string
|
||||||
|
game: string
|
||||||
|
description: string
|
||||||
|
files: string
|
||||||
|
genres: string[]
|
||||||
|
developers: string[]
|
||||||
|
publicRepositoryLink: string
|
||||||
|
players: string
|
||||||
|
release: string
|
||||||
|
modification: string
|
||||||
|
version: string
|
||||||
|
leaderboard: boolean
|
||||||
|
unityLibraryVersion?: string
|
||||||
|
|
||||||
|
thumbnail: number[]
|
||||||
|
image: number[]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
formData: FormData,
|
||||||
|
thumbnail: Uint8Array | null,
|
||||||
|
image: Uint8Array | null,
|
||||||
|
filePath: string,
|
||||||
|
id: string
|
||||||
|
) {
|
||||||
|
this.id = id
|
||||||
|
this.game = formData.get('title') as string
|
||||||
|
this.description = formData.get('description') as string
|
||||||
|
this.publicRepositoryLink = formData.get('repo') as string
|
||||||
|
this.players = formData.get('players') as string
|
||||||
|
this.release = new Date(formData.get('release') as string).toISOString().split('T')[0]
|
||||||
|
this.modification = new Date().toISOString().split('T')[0]
|
||||||
|
this.version = formData.get('version') as string
|
||||||
|
this.leaderboard = !!formData.get('leaderboard')
|
||||||
|
|
||||||
|
this.thumbnail = !!thumbnail ? Array.from(thumbnail) : []
|
||||||
|
this.image = !!image ? Array.from(image) : []
|
||||||
|
this.files = filePath
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/interfaces/auth.ts
Normal file
11
src/interfaces/auth.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export interface Role {
|
||||||
|
id: number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
id: number
|
||||||
|
role: Role
|
||||||
|
token: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
7
src/interfaces/game.ts
Normal file
7
src/interfaces/game.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface Game {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
thumbnail: any
|
||||||
|
active: boolean
|
||||||
|
}
|
||||||
7
src/interfaces/user.ts
Normal file
7
src/interfaces/user.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Role } from "./auth"
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number
|
||||||
|
email: string
|
||||||
|
role: Role
|
||||||
|
}
|
||||||
14
src/main.js
14
src/main.js
@ -1,14 +0,0 @@
|
|||||||
import './assets/main.css'
|
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
|
||||||
import { createPinia } from 'pinia'
|
|
||||||
|
|
||||||
import App from './App.vue'
|
|
||||||
import router from './router'
|
|
||||||
|
|
||||||
const app = createApp(App)
|
|
||||||
|
|
||||||
app.use(createPinia())
|
|
||||||
app.use(router)
|
|
||||||
|
|
||||||
app.mount('#app')
|
|
||||||
27
src/main.ts
Normal file
27
src/main.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import './assets/main.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
// Vuetify
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
import * as components from 'vuetify/components'
|
||||||
|
import * as directives from 'vuetify/directives'
|
||||||
|
import DateFnsAdapter from '@date-io/date-fns'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const vuetify = createVuetify({
|
||||||
|
components,
|
||||||
|
directives,
|
||||||
|
theme: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(vuetify)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
@ -1,113 +0,0 @@
|
|||||||
import {createRouter, createWebHistory} from 'vue-router'
|
|
||||||
import HomeView from '../views/HomeView.vue'
|
|
||||||
import {useAuthStore} from '@/stores/auth';
|
|
||||||
|
|
||||||
|
|
||||||
const router = createRouter({
|
|
||||||
|
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: '/',
|
|
||||||
name: 'home',
|
|
||||||
component: HomeView,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'member',
|
|
||||||
component: () => import('../views/Member.vue'),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'dashboard',
|
|
||||||
component: () => import('../components/TheWelcome.vue'),
|
|
||||||
meta: {requiresAuth: true}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/upload',
|
|
||||||
name: 'upload',
|
|
||||||
component: () => import('../views/games/UploadView.vue'),
|
|
||||||
meta: {requiresAuth: true}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/events',
|
|
||||||
name: 'events',
|
|
||||||
component: () => import('../views/mqtt/Events.vue'),
|
|
||||||
meta: {requiresAuth: true}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/games/:gameId',
|
|
||||||
name: 'game',
|
|
||||||
component: () => import('../views/games/GameView.vue'),
|
|
||||||
meta: {requiresAuth: true,},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/games',
|
|
||||||
name: 'games',
|
|
||||||
component: () => import('../views/games/GamesView.vue'),
|
|
||||||
meta: {requiresAuth: true,}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/games',
|
|
||||||
name: 'games',
|
|
||||||
component: () => import('../views/games/GamesView.vue'),
|
|
||||||
meta: {requiresAuth: true,}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/login',
|
|
||||||
name: 'login',
|
|
||||||
component: () => import('../views/auths/Login.vue'),
|
|
||||||
meta: {requiresAuth: false,}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/sign-up',
|
|
||||||
name: 'sign-up',
|
|
||||||
component: () => import('../views/auths/SignUp.vue'),
|
|
||||||
meta: {requiresAuth: false,}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'limited',
|
|
||||||
component: () => import('../views/Limited.vue'),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '/close',
|
|
||||||
name: 'close',
|
|
||||||
component: () => import('../views/players/Close.vue')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/action',
|
|
||||||
name: 'action',
|
|
||||||
component: () => import('../views/players/QrAction.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/:catchAll(.*)',
|
|
||||||
name: 'shit',
|
|
||||||
component: () => import('../views/NotFound.vue'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
|
||||||
if (to.meta.requiresAuth === undefined)
|
|
||||||
next();
|
|
||||||
|
|
||||||
if (useAuthStore().isAuth() === to.meta.requiresAuth) {
|
|
||||||
// If the condition is met, allow access to the route
|
|
||||||
next();
|
|
||||||
} else if (to.meta.requiresAuth) {
|
|
||||||
console.error('sneaky')
|
|
||||||
// If the condition is not met, redirect to another route
|
|
||||||
next('/login'); // Redirect to the login page
|
|
||||||
} else {
|
|
||||||
console.error('sneaky')
|
|
||||||
next('/')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
||||||
132
src/router/index.ts
Normal file
132
src/router/index.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import HomeView from '../views/HomeView.vue'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: HomeView,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'member',
|
||||||
|
component: () => import('../views/Member.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirect: '/games'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/games',
|
||||||
|
name: 'games',
|
||||||
|
component: () => import('../views/games/GamesView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/upload',
|
||||||
|
name: 'upload',
|
||||||
|
component: () => import('../views/games/UploadView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/manual-upload',
|
||||||
|
name: 'manual-upload',
|
||||||
|
component: () => import('../views/games/ManualUploadView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/games/:gameId',
|
||||||
|
name: 'game',
|
||||||
|
component: () => import('../views/games/GameView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
name: 'users',
|
||||||
|
component: () => import('../views/users/UsersView.vue'),
|
||||||
|
meta: { requiresAdmin: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'auth',
|
||||||
|
component: () => import('../views/auths/Auth.vue'),
|
||||||
|
meta: { requiresAuth: false },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import('../views/auths/Login.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/sign-up',
|
||||||
|
name: 'sign-up',
|
||||||
|
component: () => import('../views/auths/SignUp.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'limited',
|
||||||
|
component: () => import('../views/Limited.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/close',
|
||||||
|
name: 'close',
|
||||||
|
component: () => import('../views/players/Close.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/action',
|
||||||
|
name: 'action',
|
||||||
|
component: () => import('../views/players/QrAction.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:catchAll(.*)',
|
||||||
|
name: 'shit',
|
||||||
|
component: () => import('../views/NotFound.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
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()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn === to.meta.requiresAuth) {
|
||||||
|
// If the condition is met, allow access to the route
|
||||||
|
next()
|
||||||
|
} else if (to.meta.requiresAuth) {
|
||||||
|
console.error('sneaky')
|
||||||
|
// If the condition is not met, redirect to another route
|
||||||
|
next('/login') // Redirect to the login page
|
||||||
|
} else {
|
||||||
|
console.error('sneaky')
|
||||||
|
next('/')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
12
src/services/auth.service.ts
Normal file
12
src/services/auth.service.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { AuthDto } from '@/dtos/auth.dto'
|
||||||
|
import { BaseService } from '@/services/base-service'
|
||||||
|
|
||||||
|
export class AuthService extends BaseService {
|
||||||
|
public async login(data: AuthDto): Promise<Response> {
|
||||||
|
return this.post('login', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async signup(dto: AuthDto): Promise<Response> {
|
||||||
|
return this.post('signup', dto)
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/services/base-service.ts
Normal file
73
src/services/base-service.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
export class BaseService {
|
||||||
|
protected apiUrl: string | undefined = import.meta.env.VITE_CONJUREOS_HOST
|
||||||
|
protected baseHeaders: Record<string, string> = {
|
||||||
|
Accept: 'application/json',
|
||||||
|
'API-Version': '1'
|
||||||
|
}
|
||||||
|
protected jsonHeaders: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
protected async get<T>(path: string, headers?: HeadersInit): Promise<T> {
|
||||||
|
return (await (
|
||||||
|
await fetch(this.apiUrl + path, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
...this.baseHeaders
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).json()) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async post<T extends object>(
|
||||||
|
path: string,
|
||||||
|
body: T,
|
||||||
|
headers?: HeadersInit
|
||||||
|
): Promise<Response> {
|
||||||
|
return await fetch(this.apiUrl + path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body) as any,
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
...this.jsonHeaders,
|
||||||
|
...this.baseHeaders
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async postForm(path: string, body: FormData, headers?: HeadersInit): Promise<Response> {
|
||||||
|
return await fetch(this.apiUrl + path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: body,
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
...this.baseHeaders
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async put<T>(path: string, body: T, headers?: HeadersInit): Promise<Response> {
|
||||||
|
return await fetch(this.apiUrl + path, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
...this.jsonHeaders,
|
||||||
|
...this.baseHeaders
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async delete(path: string, headers?: HeadersInit): Promise<Response> {
|
||||||
|
return await fetch(this.apiUrl + path, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
...this.baseHeaders
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/services/game.service.ts
Normal file
62
src/services/game.service.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { BaseService } from './base-service'
|
||||||
|
import { Game } from '@/interfaces/game'
|
||||||
|
import { ValidateMetadataDto } from '@/dtos/validate-metadata.dto'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { AuthDto } from '@/dtos/auth.dto'
|
||||||
|
|
||||||
|
export class GameService extends BaseService {
|
||||||
|
public getGames(): Promise<Game[]> {
|
||||||
|
return this.get<Game[]>('games')
|
||||||
|
}
|
||||||
|
|
||||||
|
public getGame(gameId: string): Promise<Game> {
|
||||||
|
return this.get<Game>(`games/${gameId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async upload(game: FormData): Promise<Response> {
|
||||||
|
const authStr = useAuthStore()
|
||||||
|
return this.postForm(`games`, game, {
|
||||||
|
Authorization: `Bearer ${authStr.getAuth()?.token}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(game: Game): Promise<void> {
|
||||||
|
await this.put<any>(`games`, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async activate(gameId: string): Promise<void> {
|
||||||
|
await this.post<any>(`games/${gameId}/activate`, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deactivate(gameId: string): Promise<void> {
|
||||||
|
await this.post<any>(`games/${gameId}/deactivate`, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async download(gameId: string): Promise<void> {
|
||||||
|
await this.get<any>(`games/${gameId}/download`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downloadAll(): Promise<void> {
|
||||||
|
await this.get<any>(`games/download`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async patch(): Promise<void> {
|
||||||
|
await this.put<any>(`games/patch`, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async minor(): Promise<void> {
|
||||||
|
await this.put<any>(`games/minor`, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async major(): Promise<void> {
|
||||||
|
await this.put<any>(`games/major`, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async metadata(dto: ValidateMetadataDto): Promise<Response> {
|
||||||
|
return this.post(`games/metadata`, dto)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteGame(gameId: string): Promise<void> {
|
||||||
|
await this.delete(`games/${gameId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/services/user.service.ts
Normal file
12
src/services/user.service.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { User } from "@/interfaces/user";
|
||||||
|
import { BaseService } from "./base-service";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
|
export class UserService extends BaseService {
|
||||||
|
public async getUsers(): Promise<User[]> {
|
||||||
|
const authStr = useAuthStore()
|
||||||
|
return this.get<User[]>('user', {
|
||||||
|
Authorization: `Bearer ${authStr.getAuth()?.token}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/shims-vue.d.ts
vendored
Normal file
6
src/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const component: DefineComponent<object, object, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import {defineStore} from "pinia";
|
|
||||||
import {ref} from "vue";
|
|
||||||
|
|
||||||
const key = "AUTH"
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
|
||||||
/** @type {(undefined | any)} */
|
|
||||||
const auth = ref(JSON.parse(localStorage.getItem(key)) || undefined)
|
|
||||||
|
|
||||||
|
|
||||||
/** @param {(undefined | string)} auth */
|
|
||||||
function set(auth) {
|
|
||||||
this.auth = auth
|
|
||||||
if (auth)
|
|
||||||
localStorage.setItem(key, JSON.stringify(auth))
|
|
||||||
else
|
|
||||||
localStorage.removeItem(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAuth() {
|
|
||||||
return !!this.auth?.token
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuth() {
|
|
||||||
return this.auth
|
|
||||||
}
|
|
||||||
|
|
||||||
return {auth, getAuth, set, isAuth}
|
|
||||||
})
|
|
||||||
25
src/stores/auth.ts
Normal file
25
src/stores/auth.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { Ref, ref } from 'vue'
|
||||||
|
import { AuthResponse } from '@/interfaces/auth'
|
||||||
|
|
||||||
|
const key = 'AUTH'
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const localStorageAuth = localStorage.getItem(key)
|
||||||
|
const auth: Ref<AuthResponse | null> = ref(localStorageAuth ? JSON.parse(localStorageAuth) : null)
|
||||||
|
|
||||||
|
function set(_auth: AuthResponse) {
|
||||||
|
auth.value = _auth
|
||||||
|
if (_auth) localStorage.setItem(key, JSON.stringify(_auth))
|
||||||
|
else localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuth() {
|
||||||
|
return !!auth.value?.token
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuth() {
|
||||||
|
return auth.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return { auth, getAuth, set, isAuth }
|
||||||
|
})
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import {defineStore} from "pinia";
|
|
||||||
import {ref} from "vue";
|
|
||||||
|
|
||||||
export const useErrorStore = defineStore('error', () => {
|
|
||||||
/** @type {(string[])} */
|
|
||||||
const errors = ref([])
|
|
||||||
|
|
||||||
|
|
||||||
/** @param {(undefined | string)} error */
|
|
||||||
function unshift(error) {
|
|
||||||
console.error('Error:', error);
|
|
||||||
this.errors.unshift(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function getErrors() {
|
|
||||||
return this.errors
|
|
||||||
}
|
|
||||||
|
|
||||||
return { errors, getErrors, unshift}
|
|
||||||
})
|
|
||||||
18
src/stores/errors.ts
Normal file
18
src/stores/errors.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const useErrorStore = defineStore('error', () => {
|
||||||
|
/** @type {(string[])} */
|
||||||
|
const errors = ref([] as any[])
|
||||||
|
|
||||||
|
function unshift(error: string) {
|
||||||
|
console.error('Error:', error)
|
||||||
|
errors.value.unshift(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrors() {
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return { errors, getErrors, unshift }
|
||||||
|
})
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import {defineStore} from 'pinia';
|
|
||||||
import {ref} from 'vue';
|
|
||||||
|
|
||||||
export const useGamelistStore = defineStore('gamelist', () => {
|
|
||||||
/** @type {(undefined | any[])} */
|
|
||||||
const list = ref(undefined)
|
|
||||||
|
|
||||||
/** @param {(undefined | string[])} list */
|
|
||||||
function set(list) {
|
|
||||||
this.list = list
|
|
||||||
}
|
|
||||||
|
|
||||||
return {list, set}
|
|
||||||
})
|
|
||||||
13
src/stores/gamelist.ts
Normal file
13
src/stores/gamelist.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { Game } from '@/interfaces/game'
|
||||||
|
|
||||||
|
export const useGamelistStore = defineStore('gamelist', () => {
|
||||||
|
const list = ref([] as Game[])
|
||||||
|
|
||||||
|
function set(_list: Game[]) {
|
||||||
|
list.value = _list
|
||||||
|
}
|
||||||
|
|
||||||
|
return { list, set }
|
||||||
|
})
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import {defineStore} from "pinia";
|
|
||||||
import {ref} from "vue";
|
|
||||||
|
|
||||||
const key = "AUTH"
|
|
||||||
export const usePlayerAuthStore = defineStore('auth', () => {
|
|
||||||
/** @type {(undefined | string | any)} */
|
|
||||||
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() {
|
|
||||||
return !!this.auth
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuth() {
|
|
||||||
return this.auth
|
|
||||||
}
|
|
||||||
|
|
||||||
return {auth, getAuth, set, isAuth}
|
|
||||||
})
|
|
||||||
25
src/stores/player-auth.ts
Normal file
25
src/stores/player-auth.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const key = 'AUTH'
|
||||||
|
export const usePlayerAuthStore = defineStore('auth', () => {
|
||||||
|
/** @type {(undefined | string | any)} */
|
||||||
|
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() {
|
||||||
|
return !!this.auth
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuth() {
|
||||||
|
return this.auth
|
||||||
|
}
|
||||||
|
|
||||||
|
return { auth, getAuth, set, isAuth }
|
||||||
|
})
|
||||||
13
src/stores/users.ts
Normal file
13
src/stores/users.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { User } from '@/interfaces/user'
|
||||||
|
|
||||||
|
export const useUsersStore = defineStore('users', () => {
|
||||||
|
const list = ref([] as User[])
|
||||||
|
|
||||||
|
function set(_list: User[]) {
|
||||||
|
list.value = _list
|
||||||
|
}
|
||||||
|
|
||||||
|
return { list, set }
|
||||||
|
})
|
||||||
8
src/utils/blob.util.ts
Normal file
8
src/utils/blob.util.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export class BlobUtil {
|
||||||
|
static getFileName(file: File | Blob): string {
|
||||||
|
if (file instanceof File) {
|
||||||
|
return file.name
|
||||||
|
}
|
||||||
|
return (file as File).name
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/utils/conj.util.ts
Normal file
51
src/utils/conj.util.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
export class ConjUtil {
|
||||||
|
static scoreExecutable(gameTitle: string, fileName: string): number {
|
||||||
|
const normalizedTitle = gameTitle.toLowerCase().replace(/[^a-z0-9]/g, '') // Remove spaces & special chars
|
||||||
|
const normalizedFile = fileName.toLowerCase().replace(/[^a-z0-9]/g, '')
|
||||||
|
|
||||||
|
let score = 0
|
||||||
|
|
||||||
|
// ✅ High score if exact match (ignoring case & symbols)
|
||||||
|
if (normalizedFile === normalizedTitle) score += 100
|
||||||
|
|
||||||
|
// ✅ Reward filenames that contain the game title
|
||||||
|
if (normalizedFile.includes(normalizedTitle)) score += 50
|
||||||
|
|
||||||
|
// ✅ Favor files that end in the game title
|
||||||
|
if (normalizedFile.endsWith(normalizedTitle)) score += 25
|
||||||
|
|
||||||
|
// ✅ Favor filenames that are short (avoid engine-related long names)
|
||||||
|
score += Math.max(0, 20 - normalizedFile.length / 5)
|
||||||
|
|
||||||
|
// ❌ Penalize common engine-related executables
|
||||||
|
const engineFiles = [
|
||||||
|
'unitycrashhandler',
|
||||||
|
'unrealengine',
|
||||||
|
'unrealeditor',
|
||||||
|
'launcher',
|
||||||
|
'updater',
|
||||||
|
'configtool',
|
||||||
|
'settings'
|
||||||
|
]
|
||||||
|
if (engineFiles.some((engine) => normalizedFile.includes(engine))) score -= 50
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
static findBestExecutable(gameTitle: string, exeList: string[]): string | null {
|
||||||
|
if (exeList.length === 0) return null
|
||||||
|
|
||||||
|
let bestMatch = exeList[0]
|
||||||
|
let highestScore = -Infinity
|
||||||
|
|
||||||
|
for (const exe of exeList) {
|
||||||
|
const score = this.scoreExecutable(gameTitle, exe)
|
||||||
|
if (score > highestScore) {
|
||||||
|
highestScore = score
|
||||||
|
bestMatch = exe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/utils/file-saver.ts
Normal file
17
src/utils/file-saver.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export class FileSaver {
|
||||||
|
static saveFile(file: File) {
|
||||||
|
const a = document.createElement('a')
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.style.display = 'none'
|
||||||
|
const url = window.URL.createObjectURL(file)
|
||||||
|
a.href = url
|
||||||
|
a.download = file.name
|
||||||
|
a.click()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
document.body.removeChild(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
static saveBlob(blob: Blob, filename: string) {
|
||||||
|
return this.saveFile(new File([blob], filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/utils/uuid.util.ts
Normal file
9
src/utils/uuid.util.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export class GuidUtil {
|
||||||
|
static generateUUIDv4(): string {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (char) => {
|
||||||
|
const random = (Math.random() * 16) | 0
|
||||||
|
const value = char === 'x' ? random : (random & 0x3) | 0x8
|
||||||
|
return value.toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
163
src/utils/zip.util.ts
Normal file
163
src/utils/zip.util.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
export class ZipUtil {
|
||||||
|
zipParts: Uint8Array<ArrayBuffer>[] = []
|
||||||
|
centralDirectory: Uint8Array<ArrayBuffer>[] = []
|
||||||
|
offset: number = 0
|
||||||
|
|
||||||
|
private crc32(buffer: Uint8Array): number {
|
||||||
|
let table = new Uint32Array(256)
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
let c = i
|
||||||
|
for (let j = 0; j < 8; j++) {
|
||||||
|
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
|
||||||
|
}
|
||||||
|
table[i] = c
|
||||||
|
}
|
||||||
|
let crc = 0xffffffff
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
crc = (crc >>> 8) ^ table[(crc ^ buffer[i]) & 0xff]
|
||||||
|
}
|
||||||
|
return ~crc >>> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private createFileHeader(fileName: string, fileData: ArrayBuffer): Uint8Array<ArrayBuffer> {
|
||||||
|
const encodedFileName = new TextEncoder().encode(fileName)
|
||||||
|
const crc = this.crc32(new Uint8Array(fileData))
|
||||||
|
const fileSize = fileData.byteLength
|
||||||
|
const header = new Uint8Array(30 + encodedFileName.length)
|
||||||
|
const view = new DataView(header.buffer)
|
||||||
|
|
||||||
|
view.setUint32(0, 0x04034b50, true) // Local file header signature
|
||||||
|
view.setUint16(4, 20, true) // Version
|
||||||
|
view.setUint16(6, 0, true) // General purpose flag
|
||||||
|
view.setUint16(8, 0, true) // Compression method (0 = no compression)
|
||||||
|
view.setUint16(10, 0, true) // File modification time
|
||||||
|
view.setUint16(12, 0, true) // File modification date
|
||||||
|
view.setUint32(14, crc, true) // CRC-32
|
||||||
|
view.setUint32(18, fileSize, true) // Compressed size
|
||||||
|
view.setUint32(22, fileSize, true) // Uncompressed size
|
||||||
|
view.setUint16(26, encodedFileName.length, true) // Filename length
|
||||||
|
view.setUint16(28, 0, true) // Extra field length
|
||||||
|
header.set(encodedFileName, 30)
|
||||||
|
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
private createCentralDirectoryEntry(
|
||||||
|
fileName: string,
|
||||||
|
fileSize: number,
|
||||||
|
crc: number,
|
||||||
|
fileOffset: number
|
||||||
|
): Uint8Array<ArrayBuffer> {
|
||||||
|
const encodedFileName = new TextEncoder().encode(fileName)
|
||||||
|
const entry = new Uint8Array(46 + encodedFileName.length)
|
||||||
|
const view = new DataView(entry.buffer)
|
||||||
|
|
||||||
|
view.setUint32(0, 0x02014b50, true) // Central file header signature
|
||||||
|
view.setUint16(4, 20, true) // Version
|
||||||
|
view.setUint16(6, 20, true) // Version needed to extract
|
||||||
|
view.setUint16(8, 0, true) // General purpose flag
|
||||||
|
view.setUint16(10, 0, true) // Compression method (0 = no compression)
|
||||||
|
view.setUint16(12, 0, true) // File modification time
|
||||||
|
view.setUint16(14, 0, true) // File modification date
|
||||||
|
view.setUint32(16, crc, true) // CRC-32
|
||||||
|
view.setUint32(20, fileSize, true) // Compressed size
|
||||||
|
view.setUint32(24, fileSize, true) // Uncompressed size
|
||||||
|
view.setUint16(28, encodedFileName.length, true) // Filename length
|
||||||
|
view.setUint16(30, 0, true) // Extra field length
|
||||||
|
view.setUint16(32, 0, true) // File comment length
|
||||||
|
view.setUint16(34, 0, true) // Disk number start
|
||||||
|
view.setUint16(36, 0, true) // Internal file attributes
|
||||||
|
view.setUint32(38, 0, true) // External file attributes
|
||||||
|
view.setUint32(42, fileOffset, true) // Relative offset of local header
|
||||||
|
entry.set(encodedFileName, 46)
|
||||||
|
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
createEndOfCentralDirectory() {
|
||||||
|
const eocd = new Uint8Array(22)
|
||||||
|
const view = new DataView(eocd.buffer)
|
||||||
|
view.setUint32(0, 0x06054b50, true) // End of central directory signature
|
||||||
|
view.setUint16(4, 0, true) // Number of this disk
|
||||||
|
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(10, this.centralDirectory.length, true) // Total number of central directory records
|
||||||
|
view.setUint32(
|
||||||
|
12,
|
||||||
|
this.centralDirectory.reduce((sum, entry) => sum + entry.length, 0),
|
||||||
|
true
|
||||||
|
) // Size of central directory
|
||||||
|
view.setUint32(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> {
|
||||||
|
const fileData = await fileBlob.arrayBuffer()
|
||||||
|
const fileHeader = this.createFileHeader(filePath, fileData)
|
||||||
|
this.zipParts.push(fileHeader, new Uint8Array(fileData))
|
||||||
|
|
||||||
|
const fileOffset = this.offset
|
||||||
|
this.offset += fileHeader.length + fileData.byteLength
|
||||||
|
this.centralDirectory.push(
|
||||||
|
this.createCentralDirectoryEntry(
|
||||||
|
filePath,
|
||||||
|
fileData.byteLength,
|
||||||
|
this.crc32(new Uint8Array(fileData)),
|
||||||
|
fileOffset
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
getZipBlob(): Blob {
|
||||||
|
this.zipParts.push(...this.centralDirectory, this.createEndOfCentralDirectory())
|
||||||
|
return new Blob(this.zipParts, { type: 'application/zip' })
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createZipBlob(files: { filePath: string; fileBlob: Blob }[]): Promise<Blob> {
|
||||||
|
const util = new ZipUtil()
|
||||||
|
for (const { filePath, fileBlob } of files) {
|
||||||
|
await util.addFile(filePath, fileBlob)
|
||||||
|
}
|
||||||
|
return util.getZipBlob()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getZipFileTree(zipBlob: Blob): Promise<string[]> {
|
||||||
|
const fileTree: string[] = []
|
||||||
|
|
||||||
|
// Read ZIP Blob into an ArrayBuffer
|
||||||
|
const arrayBuffer = await zipBlob.arrayBuffer()
|
||||||
|
const dataView = new DataView(arrayBuffer)
|
||||||
|
let offset = arrayBuffer.byteLength - 22 // Start near the end of the ZIP file
|
||||||
|
|
||||||
|
// Find End of Central Directory Record
|
||||||
|
while (offset > 0) {
|
||||||
|
if (dataView.getUint32(offset, true) === 0x06054b50) break // Signature for End of Central Directory
|
||||||
|
offset--
|
||||||
|
}
|
||||||
|
if (offset <= 0) throw new Error('Invalid ZIP file: No End of Central Directory found')
|
||||||
|
|
||||||
|
// Read the Central Directory Offset
|
||||||
|
const centralDirOffset = dataView.getUint32(offset + 16, true)
|
||||||
|
offset = centralDirOffset
|
||||||
|
|
||||||
|
// Parse Central Directory Headers to extract filenames
|
||||||
|
while (offset < arrayBuffer.byteLength) {
|
||||||
|
if (dataView.getUint32(offset, true) !== 0x02014b50) break // Central Directory File Header
|
||||||
|
|
||||||
|
const fileNameLength = dataView.getUint16(offset + 28, true)
|
||||||
|
const extraFieldLength = dataView.getUint16(offset + 30, true)
|
||||||
|
const commentLength = dataView.getUint16(offset + 32, true)
|
||||||
|
|
||||||
|
// Extract the filename
|
||||||
|
const fileNameBytes = new Uint8Array(arrayBuffer, offset + 46, fileNameLength)
|
||||||
|
const fileName = new TextDecoder().decode(fileNameBytes)
|
||||||
|
fileTree.push(fileName)
|
||||||
|
|
||||||
|
// Move to the next entry
|
||||||
|
offset += 46 + fileNameLength + extraFieldLength + commentLength
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileTree
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,27 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {RouterLink, RouterView, useRoute} from 'vue-router'
|
import { RouterLink, RouterView } 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 Errors from '@/components/Errors.vue'
|
import Errors from '@/components/Errors.vue'
|
||||||
import {ref} from 'vue';
|
|
||||||
|
|
||||||
const authStr = useAuthStore()
|
const authStr = useAuthStore()
|
||||||
|
|
||||||
const { auth } = storeToRefs(authStr)
|
const { auth } = storeToRefs(authStr)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<img alt="Conjure logo" class="logo" src="@/assets/logo_conjure_dark.png" width="2228" height="349"/>
|
<img
|
||||||
|
alt="Conjure logo"
|
||||||
|
class="logo"
|
||||||
|
src="@/assets/logo_conjure_dark.png"
|
||||||
|
width="2228"
|
||||||
|
height="349"
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<footer>
|
<footer>
|
||||||
<router-link v-if="auth" to="/"
|
<router-link
|
||||||
class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded w-full">
|
v-if="auth"
|
||||||
|
to="/"
|
||||||
|
class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded w-full"
|
||||||
|
>
|
||||||
To dashboard
|
To dashboard
|
||||||
</router-link>
|
</router-link>
|
||||||
<Errors />
|
<Errors />
|
||||||
@ -29,7 +35,6 @@ const {auth} = storeToRefs(authStr)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
user-drag: none;
|
user-drag: none;
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
|
|||||||
@ -1,118 +1,25 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {RouterLink, RouterView, useRoute} from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
|
import Sidebar from '@/components/Sidebar.vue'
|
||||||
import {storeToRefs} from 'pinia';
|
import Header from '@/components/Header.vue'
|
||||||
import {useAuthStore} from '@/stores/auth';
|
|
||||||
import router from '@/router';
|
|
||||||
import Errors from '@/components/Errors.vue'
|
import Errors from '@/components/Errors.vue'
|
||||||
import {ref} from 'vue';
|
|
||||||
|
|
||||||
const authStr = useAuthStore()
|
|
||||||
|
|
||||||
const {auth} = storeToRefs(authStr)
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
authStr.set(undefined)
|
|
||||||
router.push('/')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<div class="flex h-screen w-screen bg-muted/40">
|
||||||
<RouterLink to="/">
|
<Sidebar />
|
||||||
<img alt="Conjure logo" class="logo" src="@/assets/logo_conjure_dark.png" width="2228" height="349"/>
|
<div class="flex flex-col flex-1">
|
||||||
</RouterLink>
|
<Header />
|
||||||
<nav v-if="auth">
|
<main class="flex-1 p-8 overflow-y-auto">
|
||||||
<RouterLink to="/games">Games</RouterLink>
|
|
||||||
<RouterLink to="/upload">Upload</RouterLink>
|
|
||||||
</nav>
|
|
||||||
<nav v-else>
|
|
||||||
<RouterLink to="/login">Login</RouterLink>
|
|
||||||
<RouterLink to="/sign-up">Sign up</RouterLink>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<footer>
|
</main>
|
||||||
<button @click="logout()" v-if="auth">Logout</button>
|
<footer class="p-4">
|
||||||
<Errors />
|
<Errors />
|
||||||
</footer>
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* Scoped styles can remain if there are any specific to Member.vue layout */
|
||||||
.logo {
|
|
||||||
user-drag: none;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
line-height: 1.5;
|
|
||||||
max-height: 100dvh;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
width: 100%;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a:first-of-type {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
padding-right: calc(var(--section-gap) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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>
|
||||||
|
|||||||
@ -1,19 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import {RouterLink, useRoute, useRouter} from 'vue-router';
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function getPathSegment() {
|
function getPathSegment() {
|
||||||
const pathSegments = route.path.split('/').filter(segment => segment !== '');
|
const pathSegments = route.path.split('/').filter((segment) => segment !== '')
|
||||||
const routes = router.getRoutes().reduce((p, c) => ({ ...p, [c.path]: true }), {})
|
const routes = router.getRoutes().reduce((p, c) => ({ ...p, [c.path]: true }), {})
|
||||||
|
|
||||||
const back = '/';
|
const back = '/'
|
||||||
for (let i = 1; i < pathSegments.length; i++) {
|
for (let i = 1; i < pathSegments.length; i++) {
|
||||||
const toTest = back + pathSegments.slice(0, -i).join('/');
|
const toTest = back + pathSegments.slice(0, -i).join('/')
|
||||||
console.log(toTest, routes[toTest])
|
console.log(toTest, routes[toTest])
|
||||||
if (!routes[toTest]) {
|
if (!routes[toTest]) {
|
||||||
continue
|
continue
|
||||||
@ -23,24 +20,22 @@ function getPathSegment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return back
|
return back
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<article>
|
<article>
|
||||||
<h1>
|
<h1>Shit not found bruh</h1>
|
||||||
Shit not found bruh
|
<RouterLink
|
||||||
</h1>
|
:to="getPathSegment()"
|
||||||
<RouterLink :to="getPathSegment()"
|
class="bg-transparent font-bold text-foreground underline underline-offset-8 hover:no-underline hover:text-foreground py-2 px-4 border border-transparent hover:border-transparent rounded hover:bg-primary"
|
||||||
class="bg-transparent font-bold text-foreground underline underline-offset-8 hover:no-underline hover:text-foreground py-2 px-4 border border-transparent hover:border-transparent rounded hover:bg-primary">
|
>
|
||||||
go back
|
go back
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|
||||||
article {
|
article {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -64,5 +59,4 @@ ul {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
129
src/views/auths/Auth.vue
Normal file
129
src/views/auths/Auth.vue
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterLink, RouterView } from 'vue-router'
|
||||||
|
import Errors from '@/components/Errors.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="auth">
|
||||||
|
<div id="auth-content">
|
||||||
|
<header>
|
||||||
|
<RouterLink to="/">
|
||||||
|
<img
|
||||||
|
alt="Conjure logo"
|
||||||
|
class="logo"
|
||||||
|
src="@/assets/logo_conjure_dark.png"
|
||||||
|
width="2228"
|
||||||
|
height="349"
|
||||||
|
/>
|
||||||
|
</RouterLink>
|
||||||
|
<nav>
|
||||||
|
<RouterLink to="/login">Login</RouterLink>
|
||||||
|
<RouterLink to="/sign-up">Sign up</RouterLink>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<RouterView />
|
||||||
|
<footer>
|
||||||
|
<Errors />
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
#auth {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
gap: 1rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
user-drag: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
line-height: 1.5;
|
||||||
|
max-height: 100dvh;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
width: 100%;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:first-of-type {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
padding-right: calc(var(--section-gap) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#auth-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,54 +1,53 @@
|
|||||||
<script setup>
|
<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'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import Loader from '@/components/Loader.vue'
|
import Loader from '@/components/Loader.vue'
|
||||||
|
import { AuthService } from '@/services/auth.service'
|
||||||
const apiHost = import.meta.env.VITE_CONJUREOS_HOST
|
import { AuthDto } from '@/dtos/auth.dto'
|
||||||
|
|
||||||
const errorStore = useErrorStore()
|
const errorStore = useErrorStore()
|
||||||
|
const authService = new AuthService()
|
||||||
const isLoginIn = ref(false)
|
const isLoginIn = ref(false)
|
||||||
const login = (form) => {
|
const login = async (form: HTMLFormElement) => {
|
||||||
const formData = new FormData(form)
|
const formData = new FormData(form)
|
||||||
isLoginIn.value = true
|
isLoginIn.value = true
|
||||||
fetch(apiHost + 'login', {
|
|
||||||
method: 'POST',
|
const dto = new AuthDto(formData)
|
||||||
body: formData
|
const response = await authService.login(dto).catch((error) => {
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
isLoginIn.value = false
|
|
||||||
if (response.status !== 200)
|
|
||||||
return response.text().then(error => {
|
|
||||||
throw new Error(error)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return response.text()
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
useAuthStore().set(JSON.parse(result))
|
|
||||||
router.push('/')
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
isLoginIn.value = false
|
isLoginIn.value = false
|
||||||
errorStore.unshift(error)
|
errorStore.unshift(error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
isLoginIn.value = false
|
||||||
|
if (!response) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
useAuthStore().set(result)
|
||||||
|
router.push('/')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<article>
|
<article>
|
||||||
<h1>Login</h1>
|
<h1>Login</h1>
|
||||||
<form ref="loginForm" enctype="multipart/form-data" @submit.prevent="login($refs.loginForm)">
|
<form
|
||||||
<label for="username">username</label>
|
ref="loginForm"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
@submit.prevent="login($refs.loginForm as HTMLFormElement)"
|
||||||
|
>
|
||||||
|
<label for="email">Email</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
type="text"
|
type="email"
|
||||||
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="email"
|
||||||
id="username"
|
id="email"
|
||||||
/>
|
/>
|
||||||
<label for="password">password</label>
|
<label for="password">Password</label>
|
||||||
<input
|
<input
|
||||||
required
|
required
|
||||||
type="password"
|
type="password"
|
||||||
|
|||||||
@ -1,56 +1,72 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { useErrorStore } from '@/stores/errors'
|
import { useErrorStore } from '@/stores/errors'
|
||||||
import {ref} from "vue";
|
import { ref } from 'vue'
|
||||||
import {usePlayerAuthStore} from '@/stores/player-auth';
|
import { usePlayerAuthStore } from '@/stores/player-auth'
|
||||||
import router from '@/router';
|
import router from '@/router'
|
||||||
|
|
||||||
const apiHost = import.meta.env.VITE_CONJUREOS_HOST
|
const apiHost = import.meta.env.VITE_CONJUREOS_HOST
|
||||||
|
|
||||||
const errorStore = useErrorStore()
|
const errorStore = useErrorStore()
|
||||||
|
|
||||||
const isLoginIn = ref(false)
|
const isLoginIn = ref(false)
|
||||||
const signup = (form) => {
|
const signup = (form: HTMLFormElement) => {
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form)
|
||||||
isLoginIn.value = true
|
isLoginIn.value = true
|
||||||
|
const myHeaders = new Headers()
|
||||||
|
myHeaders.append('API-Version', '1')
|
||||||
fetch(apiHost + 'signup', {
|
fetch(apiHost + 'signup', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
|
headers: myHeaders
|
||||||
})
|
})
|
||||||
.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) => {
|
||||||
isLoginIn.value = false
|
isLoginIn.value = false
|
||||||
|
console.log(result)
|
||||||
usePlayerAuthStore().set(JSON.parse(result))
|
usePlayerAuthStore().set(JSON.parse(result))
|
||||||
router.push('/')
|
router.push('/')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
isLoginIn.value = false
|
isLoginIn.value = false
|
||||||
errorStore.unshift(error)
|
errorStore.unshift(error)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<article>
|
<article>
|
||||||
<h1>Sign up</h1>
|
<h1>Sign up</h1>
|
||||||
<form ref="signupForm" enctype="multipart/form-data" @submit.prevent="signup($refs.signupForm)">
|
<form
|
||||||
<label for="username">username</label>
|
ref="signupForm"
|
||||||
<input required type="text"
|
enctype="multipart/form-data"
|
||||||
|
@submit.prevent="signup($refs.signupForm as HTMLFormElement)"
|
||||||
|
>
|
||||||
|
<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"
|
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||||
name="username" id="username"/>
|
name="email"
|
||||||
<label for="password">password</label>
|
id="email"
|
||||||
<input required type="password"
|
/>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
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" id="password"/>
|
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>
|
||||||
|
|||||||
@ -1,140 +1,135 @@
|
|||||||
<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'
|
||||||
|
|
||||||
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)
|
||||||
console.log(route, route.meta)
|
const confirmingDeletion = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetch(apiHost + 'games/' + gameId.value, {
|
fetch(apiHost + 'games/' + gameId.value, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
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() {
|
||||||
*
|
console.log('Try')
|
||||||
*/
|
confirmingDeletion.value = true
|
||||||
function activate() {
|
new Promise((_) => setTimeout(_, 1000)).then(() => {
|
||||||
fetch(apiHost + 'games/' + gameId.value + '/activate', {
|
confirmingDeletion.value = false
|
||||||
method: 'POST', headers: {
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteGame() {
|
||||||
|
console.log('Delete')
|
||||||
|
fetch(`${apiHost}games/${gameId.value}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${auth.value.token}`,
|
Authorization: `Bearer ${auth.value.token}`,
|
||||||
},
|
'API-Version': 1
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status !== 204)
|
if (response.ok) {
|
||||||
return response.json().then((errorBody) => {
|
alert('Deleted')
|
||||||
throw new Error(errorBody);
|
router.back()
|
||||||
});
|
} else {
|
||||||
|
alert('Error deleting game')
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
.then((result) => {
|
|
||||||
game.value = {...game.value, active: true}
|
|
||||||
isActivating.value = false
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
errorStore.unshift(error)
|
errorStore.unshift(error)
|
||||||
isActivating.value = false
|
})
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function toggleActivation(state) {
|
||||||
*
|
|
||||||
*/
|
|
||||||
function deactivate() {
|
|
||||||
isActivating.value = true
|
isActivating.value = true
|
||||||
fetch(apiHost + 'games/' + gameId.value + '/deactivate', {
|
fetch(`${apiHost}games/${gameId.value}/${state ? 'activate' : 'deactivate'}`, {
|
||||||
method: 'POST', headers: {
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${auth.value.token}`,
|
Authorization: `Bearer ${auth.value.token}`,
|
||||||
},
|
'API-Version': 1
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status !== 204)
|
if (response.status !== 204)
|
||||||
return response.json().then((errorBody) => {
|
return response.json().then((errorBody) => {
|
||||||
throw new Error(errorBody);
|
throw new Error(errorBody)
|
||||||
});
|
})
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then(() => {
|
||||||
game.value = {...game.value, active: false}
|
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>
|
||||||
<template 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 v-if="game.active" @click="deactivate()"
|
<button
|
||||||
class="bg-transparent font-bold text-primary underline underline-offset-8 hover:no-underline hover:text-primary py-2 px-4 border border-transparent hover:border-transparent rounded hover:bg-primary">
|
:class="
|
||||||
deactivate
|
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"
|
||||||
|
@click="toggleActivation(!game.active)"
|
||||||
|
:disabled="isActivating"
|
||||||
|
>
|
||||||
|
{{ game.active ? 'Deactivate' : 'Activate' }}
|
||||||
</button>
|
</button>
|
||||||
<button v-else @click="activate()"
|
|
||||||
class="bg-transparent font-bold text-primary underline underline-offset-8 hover:no-underline hover:text-primary py-2 px-4 border border-transparent hover:border-transparent rounded hover:bg-primary">
|
<button
|
||||||
activate
|
class="font-bold py-2 px-4 m-2 rounded bg-red-500 text-white hover:bg-red-700"
|
||||||
|
@click="confirmingDeletion ? deleteGame() : tryDeleteGame()"
|
||||||
|
>
|
||||||
|
{{ confirmingDeletion ? 'Sure?' : 'Delete' }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</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;
|
||||||
align-items: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
li {
|
|
||||||
background-color: rgba(var(--vt-c-payne), 0.6);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 2fr;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-height: 6rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@ -1,57 +1,49 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted} from 'vue';
|
import { onMounted } from 'vue'
|
||||||
import {useGamelistStore} from '@/stores/gamelist';
|
import { useGamelistStore } from '@/stores/gamelist'
|
||||||
import {storeToRefs} from 'pinia';
|
import { storeToRefs } from 'pinia'
|
||||||
import { useErrorStore } from '@/stores/errors'
|
import { useErrorStore } from '@/stores/errors'
|
||||||
import Loader from '@/components/Loader.vue';
|
import Loader from '@/components/Loader.vue'
|
||||||
import {RouterLink} from 'vue-router';
|
import { RouterLink } from 'vue-router'
|
||||||
|
import { GameService } from '@/services/game.service'
|
||||||
|
import { DownloadIcon } from 'lucide-vue-next'
|
||||||
|
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
|
||||||
const gamelistStore = useGamelistStore()
|
const gamelistStore = useGamelistStore()
|
||||||
|
|
||||||
const { list: gamelist } = storeToRefs(gamelistStore)
|
const { list: gamelist } = storeToRefs(gamelistStore)
|
||||||
|
const gamesService = new GameService()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
fetch(apiHost + 'games', {
|
const games = await gamesService.getGames().catch((error) => {
|
||||||
method: 'GET',
|
console.error('Error:', error)
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((result) => {
|
|
||||||
gamelistStore.set(result)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
errorStore.unshift(error)
|
errorStore.unshift(error)
|
||||||
});
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
if (games) {
|
||||||
* @param {string} name
|
gamelistStore.set(games)
|
||||||
*/
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function download(name) {
|
function download(name) {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a')
|
||||||
a.href = apiHost + 'games/' + name + '/download';
|
a.href = apiHost + 'games/' + name + '/download'
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
// Programmatically click the anchor element to start the download
|
document.body.removeChild(a)
|
||||||
a.click();
|
|
||||||
|
|
||||||
// Clean up by removing the anchor element
|
|
||||||
document.body.removeChild(a);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadAll() {
|
function downloadAll() {
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a')
|
||||||
a.href = apiHost + 'games/download';
|
a.href = apiHost + 'games/download'
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
}
|
||||||
|
|
||||||
// Programmatically click the anchor element to start the download
|
function newGame() {
|
||||||
a.click();
|
router.push('/upload')
|
||||||
|
|
||||||
// Clean up by removing the anchor element
|
|
||||||
document.body.removeChild(a);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -59,63 +51,65 @@ function downloadAll() {
|
|||||||
<article>
|
<article>
|
||||||
<loader v-if="gamelist === undefined"></loader>
|
<loader v-if="gamelist === undefined"></loader>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<header>
|
<header class="flex flex-row justify-between items-center">
|
||||||
|
<h1 class="text-foreground">My Games</h1>
|
||||||
|
<div class="flex gap-3">
|
||||||
<button
|
<button
|
||||||
class="bg-transparent font-bold text-foreground underline underline-offset-8 hover:no-underline py-2 px-4 border border-primary rounded hover:bg-primary"
|
class="bg-transparent font-bold text-foreground py-2 px-4 border border-primary rounded hover:bg-primary"
|
||||||
@click="downloadAll()"
|
@click="downloadAll()"
|
||||||
>
|
>
|
||||||
Download all
|
Download All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bg-transparent font-bold text-foreground py-2 px-4 border border-primary rounded hover:bg-primary"
|
||||||
|
@click="newGame()"
|
||||||
|
>
|
||||||
|
New Game
|
||||||
</button>
|
</button>
|
||||||
</header>
|
|
||||||
<ul>
|
|
||||||
<li class="game rounded-lg p-4" v-for="(item) in gamelist" :key="item.id">
|
|
||||||
<img v-if="item.thumbnail" :src="'data:image/png;base64,'+item.thumbnail" alt="thumbnail"/>
|
|
||||||
<span v-else></span>
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<p>{{ item.description }}</p>
|
|
||||||
<i class="ml-auto" v-if="item.active">active</i>
|
|
||||||
</div>
|
</div>
|
||||||
<h2>{{ item.game }}</h2>
|
</header>
|
||||||
|
<ul class="flex flex-col gap-4">
|
||||||
|
<li
|
||||||
|
v-for="item in gamelist"
|
||||||
|
:key="item.id"
|
||||||
|
class="game-item border border-gray-300 rounded-lg p-4 flex items-center gap-4 hover:border-2"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
<RouterLink :to="'/games/' + item.id"
|
<h2 class="font-bold">{{ item.game }}</h2>
|
||||||
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">
|
<p class="text-primary">{{ item.description }}</p>
|
||||||
open
|
<i v-if="item.active" class="text-green-500 pt-3">Active</i>
|
||||||
|
</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<button
|
<button
|
||||||
class="bg-transparent font-bold text-foreground underline underline-offset-8 hover:no-underline hover:text-foreground py-2 px-4 border border-transparent hover:border-transparent rounded hover:bg-primary"
|
class="p-2 rounded-full hover:bg-gray-200 text-green-500 hover:text-green-700"
|
||||||
type="button" @click="download(item.id + '.conj')">Download
|
@click="download(item.id)"
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped>
|
||||||
|
.game-item:hover {
|
||||||
|
margin: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
article {
|
article {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
li {
|
|
||||||
background-color: rgba(var(--vt-c-payne), 0.6);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 2fr;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-height: 6rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
234
src/views/games/ManualUploadView.vue
Normal file
234
src/views/games/ManualUploadView.vue
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import DateInput from '@/components/DateInput.vue'
|
||||||
|
import { UploadGameDto } from '@/dtos/upload-game.dto'
|
||||||
|
import { GameService } from '@/services/game.service'
|
||||||
|
import { ValidateMetadataDto } from '@/dtos/validate-metadata.dto'
|
||||||
|
import { ZipUtil } from '@/utils/zip.util'
|
||||||
|
import { BlobUtil } from '@/utils/blob.util'
|
||||||
|
import { FileSaver } from '@/utils/file-saver'
|
||||||
|
import { GuidUtil } from '@/utils/uuid.util'
|
||||||
|
import { ConjUtil } from '@/utils/conj.util'
|
||||||
|
|
||||||
|
let playerValue = ref('')
|
||||||
|
let versionValue = ref('')
|
||||||
|
let uploadForm = ref()
|
||||||
|
|
||||||
|
// onMounted(() => {
|
||||||
|
// testData();
|
||||||
|
// });
|
||||||
|
|
||||||
|
function validatePlayersInput(): void {
|
||||||
|
let value = playerValue.value.replace(/[^1-2]/g, '') // Allow only digits and hyphen
|
||||||
|
value = value.slice(0, 3) // Ensure max length of 3
|
||||||
|
if (value.length === 2 && !value.includes('-')) {
|
||||||
|
value = value.charAt(0) + '-' + value.charAt(1) // Auto-insert hyphen
|
||||||
|
}
|
||||||
|
if (value.length === 3) {
|
||||||
|
const min = +value.charAt(0)
|
||||||
|
let max = +value.charAt(2)
|
||||||
|
if (max < min) max = min
|
||||||
|
value = `${min}-${max}`
|
||||||
|
}
|
||||||
|
playerValue.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateVersionInput(): void {
|
||||||
|
versionValue.value = versionValue.value.replace(/[^0-9.]/g, '') // Allow only digits and hyphen
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitForm(): Promise<void> {
|
||||||
|
console.log('Submitting...')
|
||||||
|
const formData = new FormData(uploadForm.value)
|
||||||
|
const files = (document.getElementById('medias') as HTMLInputElement).files
|
||||||
|
let thumbnail = null
|
||||||
|
let tbnFile = null
|
||||||
|
let image = null
|
||||||
|
let imageFile = null
|
||||||
|
if (!!files && files.length > 0) {
|
||||||
|
tbnFile = files[0]
|
||||||
|
thumbnail = new Uint8Array(await tbnFile.arrayBuffer())
|
||||||
|
}
|
||||||
|
if (!!files && files.length > 1) {
|
||||||
|
imageFile = files[1]
|
||||||
|
image = new Uint8Array(await imageFile.arrayBuffer())
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = formData.get('game') as File
|
||||||
|
const gameSplit = game.name.split('.')
|
||||||
|
const ext = gameSplit[gameSplit.length - 1]
|
||||||
|
let gamePath = ''
|
||||||
|
if (ext === 'zip') {
|
||||||
|
let fileTree = await ZipUtil.getZipFileTree(game)
|
||||||
|
fileTree = fileTree.filter((file) => file.includes('.exe'))
|
||||||
|
const title = formData.get('title') as string
|
||||||
|
const bestMatch = ConjUtil.findBestExecutable(title, fileTree)
|
||||||
|
if (!bestMatch) throw new Error('No executable found in zip file')
|
||||||
|
const folder = BlobUtil.getFileName(game).split('.')[0]
|
||||||
|
gamePath = `${folder}\\${bestMatch}`
|
||||||
|
} else if (ext === 'exe') {
|
||||||
|
gamePath = game.name
|
||||||
|
} else {
|
||||||
|
throw new Error('Unsupported file type')
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = GuidUtil.generateUUIDv4()
|
||||||
|
const dto = new ValidateMetadataDto(formData, thumbnail, image, gamePath, id)
|
||||||
|
const service = new GameService()
|
||||||
|
const response = await service.metadata(dto)
|
||||||
|
let metadataText = await response.text()
|
||||||
|
console.log(metadataText)
|
||||||
|
const metadataObj: any = {}
|
||||||
|
let metadataLines = metadataText.split('\n')
|
||||||
|
metadataLines = metadataLines.slice(0, metadataLines.length - 1)
|
||||||
|
|
||||||
|
for (const line of metadataLines) {
|
||||||
|
const lineSplit = line.split(':')
|
||||||
|
metadataObj[lineSplit[0].trim()] = lineSplit[1].trim()
|
||||||
|
}
|
||||||
|
if (!!tbnFile) metadataObj['thumbnailPath'] = `medias\\${BlobUtil.getFileName(tbnFile)}`
|
||||||
|
if (!!imageFile) metadataObj['imagePath'] = `medias\\${BlobUtil.getFileName(imageFile)}`
|
||||||
|
|
||||||
|
console.log(metadataObj)
|
||||||
|
metadataText = Object.keys(metadataObj)
|
||||||
|
.map((key) => [key, metadataObj[key]].join(': '))
|
||||||
|
.join('\n')
|
||||||
|
console.log(metadataText)
|
||||||
|
const metadataBlob = new Blob([metadataText], { type: 'text/plain' })
|
||||||
|
|
||||||
|
const zipUtil = new ZipUtil()
|
||||||
|
if (!!files) {
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i]
|
||||||
|
const fileName = BlobUtil.getFileName(file)
|
||||||
|
await zipUtil.addFile(`medias/${fileName}`, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await zipUtil.addFile('metadata.txt', metadataBlob)
|
||||||
|
await zipUtil.addFile(BlobUtil.getFileName(game), game)
|
||||||
|
const zipBlob = zipUtil.getZipBlob()
|
||||||
|
const zipFile = new File([zipBlob], `${id}.conj`)
|
||||||
|
FileSaver.saveFile(zipFile)
|
||||||
|
|
||||||
|
// TODO remove form data
|
||||||
|
const conjUploadForm = new FormData()
|
||||||
|
conjUploadForm.append('file', zipFile)
|
||||||
|
const uploadResponse = await service.upload(conjUploadForm)
|
||||||
|
//alert(await uploadResponse.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
function testData(): void {
|
||||||
|
;(document.getElementById('title') as HTMLInputElement).value = 'Game Test'
|
||||||
|
;(document.getElementById('description') as HTMLInputElement).value = 'Ceci est un test'
|
||||||
|
;(document.getElementById('genres') as HTMLInputElement).value = 'Action, Adventure'
|
||||||
|
;(document.getElementById('devs') as HTMLInputElement).value = 'Jean,Yussef'
|
||||||
|
;(document.getElementById('repo') as HTMLInputElement).value = 'https://repo.com'
|
||||||
|
;(document.getElementById('players') as HTMLInputElement).value = '1-2'
|
||||||
|
;(document.getElementById('release') as HTMLInputElement).value = '01/01/2021'
|
||||||
|
;(document.getElementById('version') as HTMLInputElement).value = '1.1.1'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article>
|
||||||
|
<h1 class="text-foreground">Manual Upload</h1>
|
||||||
|
<div class="text-foreground w-full my-2 underline">
|
||||||
|
<RouterLink to="upload" class="hover:color-blue-300">Or upload a .conj</RouterLink>
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
ref="uploadForm"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
class="flex flex-col gap-2"
|
||||||
|
@submit.prevent="submitForm"
|
||||||
|
>
|
||||||
|
<label for="title">Title</label>
|
||||||
|
<input
|
||||||
|
name="title"
|
||||||
|
id="title"
|
||||||
|
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea
|
||||||
|
name="description"
|
||||||
|
id="description"
|
||||||
|
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="medias">Media</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="medias"
|
||||||
|
id="medias"
|
||||||
|
accept="video/quicktime,image/png,image/jpeg,video/mp4"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="game">Game</label>
|
||||||
|
<input type="file" name="game" id="game" accept="application/zip,application/exe" />
|
||||||
|
|
||||||
|
<label for="genres">Genres</label>
|
||||||
|
<input
|
||||||
|
name="genres"
|
||||||
|
id="genres"
|
||||||
|
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||||
|
placeholder="Action, Adventure"
|
||||||
|
/>
|
||||||
|
<label for="devs">Devs</label>
|
||||||
|
<input
|
||||||
|
name="devs"
|
||||||
|
id="devs"
|
||||||
|
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||||
|
placeholder="Jean, Yussef"
|
||||||
|
/>
|
||||||
|
<label for="repo">Public Repository</label>
|
||||||
|
<input
|
||||||
|
name="repo"
|
||||||
|
id="repo"
|
||||||
|
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||||
|
placeholder="https://github.com/..."
|
||||||
|
/>
|
||||||
|
<label for="players">Player Count</label>
|
||||||
|
<input
|
||||||
|
name="players"
|
||||||
|
id="players"
|
||||||
|
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||||
|
v-model="playerValue"
|
||||||
|
placeholder="0-0"
|
||||||
|
pattern="\d-\d"
|
||||||
|
@input="validatePlayersInput"
|
||||||
|
maxlength="3"
|
||||||
|
/>
|
||||||
|
<!-- <date-input></date-input>-->
|
||||||
|
<label for="release">Release Date</label>
|
||||||
|
<input
|
||||||
|
name="release"
|
||||||
|
id="release"
|
||||||
|
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||||
|
placeholder="00/00/0000"
|
||||||
|
/>
|
||||||
|
<label for="version">Version</label>
|
||||||
|
<input
|
||||||
|
name="version"
|
||||||
|
id="version"
|
||||||
|
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||||
|
v-model="versionValue"
|
||||||
|
placeholder="0.0.0"
|
||||||
|
pattern="\d.\d.\d"
|
||||||
|
@input="validateVersionInput"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="leaderboard">Leaderboard</label>
|
||||||
|
<input name="leaderboard" id="leaderboard" type="checkbox" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="bg-transparent font-bold text-foreground py-2 px-4 my-2 border border-primary rounded hover:bg-primary"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@ -16,12 +16,14 @@ 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()))
|
||||||
|
|
||||||
fetch(apiHost + 'games', {
|
fetch(apiHost + 'games', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: authStr.getAuth()
|
Authorization: `Bearer ${authStr.getAuth().token}`,
|
||||||
|
'API-Version': 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((response) => response.text())
|
.then((response) => response.text())
|
||||||
@ -44,7 +46,12 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<article>
|
<article>
|
||||||
<h1 class="text-foreground">Upload</h1>
|
<h1 class="text-foreground">Upload</h1>
|
||||||
<form ref="uploadForm" enctype="multipart/form-data" @submit.prevent="submitForm">
|
<form
|
||||||
|
ref="uploadForm"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
class="flex flex-col gap-2"
|
||||||
|
@submit.prevent="submitForm"
|
||||||
|
>
|
||||||
<!-- <div class="name-input-wrapper">-->
|
<!-- <div class="name-input-wrapper">-->
|
||||||
<!-- <label for="name" class="block text-sm font-medium text-gray-700">Name:</label>-->
|
<!-- <label for="name" class="block text-sm font-medium text-gray-700">Name:</label>-->
|
||||||
<!-- <input type="text" name="name" id="name" required v-model="textInput"-->
|
<!-- <input type="text" name="name" id="name" required v-model="textInput"-->
|
||||||
@ -57,6 +64,7 @@ export default {
|
|||||||
name="file"
|
name="file"
|
||||||
id="file"
|
id="file"
|
||||||
accept=".conj"
|
accept=".conj"
|
||||||
|
class="cursor-pointer"
|
||||||
@change="filesChanges"
|
@change="filesChanges"
|
||||||
/>
|
/>
|
||||||
<label>
|
<label>
|
||||||
@ -65,8 +73,15 @@ export default {
|
|||||||
<em class="file-name" v-if="!!selectedFiles?.length">{{ selectedFiles[0].name }}</em>
|
<em class="file-name" v-if="!!selectedFiles?.length">{{ selectedFiles[0].name }}</em>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="text-foreground w-full my-2 underline">
|
||||||
|
<RouterLink to="manual-upload" class="hover:color-blue-300"
|
||||||
|
>Or manually enter your metadata</RouterLink
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="bg-transparent text-gray-700 font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded"
|
class="bg-transparent font-bold text-foreground py-2 px-4 border border-primary rounded hover:bg-primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Upload
|
Upload
|
||||||
@ -78,9 +93,6 @@ export default {
|
|||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
form {
|
form {
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-upload-wrapper {
|
.file-upload-wrapper {
|
||||||
|
|||||||
@ -1,45 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,11 +1,7 @@
|
|||||||
<script setup>
|
<script setup></script>
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>Please close this page.</h1>
|
<h1>Please close this page.</h1>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -1,37 +1,38 @@
|
|||||||
<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()
|
||||||
|
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
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
isLoginIn.value = false
|
isLoginIn.value = false
|
||||||
if (response.status !== 200)
|
if (response.status !== 200)
|
||||||
return response.text().then(error => {
|
return response.text().then((error) => {
|
||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
}
|
})
|
||||||
)
|
|
||||||
return response.text()
|
return response.text()
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
|||||||
49
src/views/users/UsersView.vue
Normal file
49
src/views/users/UsersView.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">Users Management</h1>
|
||||||
|
|
||||||
|
<loader v-if="users === undefined"></loader>
|
||||||
|
<div class="mt-4" v-else>
|
||||||
|
<table class="min-w-full border-gray-200">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="py-2 px-4 border-b text-left">Email</th>
|
||||||
|
<th class="py-2 px-4 border-b text-left">Role</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="user in users" :key="user.id">
|
||||||
|
<td class="py-2 px-4 border-b">{{ user.email }}</td>
|
||||||
|
<td class="py-2 px-4 border-b">{{ user.role.label }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useErrorStore } from '@/stores/errors';
|
||||||
|
import { UserService } from '@/services/user.service';
|
||||||
|
import { useUsersStore } from '@/stores/users';
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import Loader from '@/components/Loader.vue'
|
||||||
|
|
||||||
|
const errorStore = useErrorStore()
|
||||||
|
const apiHost = import.meta.env.VITE_CONJUREOS_HOST
|
||||||
|
const usersStore = useUsersStore()
|
||||||
|
const { list: users } = storeToRefs(usersStore)
|
||||||
|
const userService = new UserService()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const users = await userService.getUsers().catch((error) => {
|
||||||
|
console.error('Error:', error)
|
||||||
|
errorStore.unshift(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (users) {
|
||||||
|
usersStore.set(users)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
35
tsconfig.json
Normal file
35
tsconfig.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": false,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -3,23 +3,24 @@ import {fileURLToPath, URL} from 'node:url'
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports,no-undef
|
||||||
process.env.VUE_APP_VERSION = require('./package.json').version
|
process.env.VUE_APP_VERSION = require('./package.json').version
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue()
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
terserOptions: {
|
terserOptions: {
|
||||||
compress: {
|
compress: {
|
||||||
drop_console: false,
|
drop_console: false
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user