Compare commits
3 Commits
master
...
tristan-wi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
|
||||
20
eslint.config.cjs
Normal file
20
eslint.config.cjs
Normal file
@ -0,0 +1,20 @@
|
||||
/* eslint-env node */
|
||||
// require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-prettier/skip-formatting',
|
||||
'plugin:@typescript-eslint/recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
parser: '@typescript-eslint/parser'
|
||||
},
|
||||
plugins: [
|
||||
'@typescript-eslint/eslint-plugin'
|
||||
]
|
||||
}
|
||||
@ -8,6 +8,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
8330
package-lock.json
generated
8330
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@ -4,37 +4,47 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@date-io/date-fns": "^3.2.1",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/forms": "^0.5.6",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"lucide-vue-next": "^0.479.0",
|
||||
"mqtt": "^5.3.3",
|
||||
"pinia": "^2.1.6",
|
||||
"vite-plugin-vuetify": "^2.1.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
"vue-router": "^4.2.4",
|
||||
"vuetify": "^3.7.16"
|
||||
},
|
||||
"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",
|
||||
"@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",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.68.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"vite": "^4.4.9",
|
||||
"vitest": "^0.34.4"
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^5.4.14",
|
||||
"vitest": "^3.0.8",
|
||||
"vue-tsc": "^2.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,19 +29,6 @@ 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 */
|
||||
h1 {
|
||||
|
||||
74
src/components/DateInput.vue
Normal file
74
src/components/DateInput.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<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>
|
||||
9
src/dtos/auth.dto.ts
Normal file
9
src/dtos/auth.dto.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export class AuthDto {
|
||||
username: string;
|
||||
password: string;
|
||||
|
||||
public constructor(form: FormData) {
|
||||
this.username = form.get("username") as string;
|
||||
this.password = form.get("password") as string;
|
||||
}
|
||||
}
|
||||
50
src/dtos/upload-game.dto.ts
Normal file
50
src/dtos/upload-game.dto.ts
Normal file
@ -0,0 +1,50 @@
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
39
src/dtos/validate-metadata.dto.ts
Normal file
39
src/dtos/validate-metadata.dto.ts
Normal file
@ -0,0 +1,39 @@
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
6
src/interfaces/auth.ts
Normal file
6
src/interfaces/auth.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface AuthResponse {
|
||||
id: number;
|
||||
roles: string[];
|
||||
token: string;
|
||||
username: 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
|
||||
}
|
||||
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')
|
||||
28
src/main.ts
Normal file
28
src/main.ts
Normal file
@ -0,0 +1,28 @@
|
||||
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
|
||||
115
src/router/index.ts
Normal file
115
src/router/index.ts
Normal file
@ -0,0 +1,115 @@
|
||||
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: '/events',
|
||||
name: 'events',
|
||||
component: () => import('../views/mqtt/Events.vue'),
|
||||
},
|
||||
{
|
||||
path: '/games/:gameId',
|
||||
name: 'game',
|
||||
component: () => import('../views/games/GameView.vue'),
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
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) => {
|
||||
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
|
||||
13
src/services/auth.service.ts
Normal file
13
src/services/auth.service.ts
Normal file
@ -0,0 +1,13 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
69
src/services/base-service.ts
Normal file
69
src/services/base-service.ts
Normal file
@ -0,0 +1,69 @@
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
63
src/services/game.service.ts
Normal file
63
src/services/game.service.ts
Normal file
@ -0,0 +1,63 @@
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
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}
|
||||
})
|
||||
29
src/stores/auth.ts
Normal file
29
src/stores/auth.ts
Normal file
@ -0,0 +1,29 @@
|
||||
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 }
|
||||
})
|
||||
@ -3,18 +3,17 @@ import {ref} from "vue";
|
||||
|
||||
export const useErrorStore = defineStore('error', () => {
|
||||
/** @type {(string[])} */
|
||||
const errors = ref([])
|
||||
const errors = ref([] as any[])
|
||||
|
||||
|
||||
/** @param {(undefined | string)} error */
|
||||
function unshift(error) {
|
||||
function unshift(error: string) {
|
||||
console.error('Error:', error);
|
||||
this.errors.unshift(error)
|
||||
errors.value.unshift(error)
|
||||
}
|
||||
|
||||
|
||||
function getErrors() {
|
||||
return this.errors
|
||||
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 }
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
46
src/utils/conj.util.ts
Normal file
46
src/utils/conj.util.ts
Normal file
@ -0,0 +1,46 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
147
src/utils/zip.util.ts
Normal file
147
src/utils/zip.util.ts
Normal file
@ -0,0 +1,147 @@
|
||||
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,30 +1,28 @@
|
||||
<script setup>
|
||||
import {RouterLink, RouterView, useRoute} from 'vue-router'
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
|
||||
import {storeToRefs} from 'pinia';
|
||||
import {useAuthStore} from '@/stores/auth';
|
||||
import router from '@/router';
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import Errors from '@/components/Errors.vue'
|
||||
import {ref} from 'vue';
|
||||
|
||||
const authStr = useAuthStore()
|
||||
|
||||
const {auth} = storeToRefs(authStr)
|
||||
const { auth } = storeToRefs(authStr)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
|
||||
<RouterView/>
|
||||
<RouterView />
|
||||
<footer>
|
||||
<router-link 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
|
||||
</router-link>
|
||||
<Errors/>
|
||||
<Errors />
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
import {RouterLink, RouterView, useRoute} from 'vue-router'
|
||||
import {RouterLink, RouterView } from 'vue-router'
|
||||
|
||||
import {storeToRefs} from 'pinia';
|
||||
import {useAuthStore} from '@/stores/auth';
|
||||
import router from '@/router';
|
||||
import Errors from '@/components/Errors.vue'
|
||||
import {ref} from 'vue';
|
||||
import { LogOutIcon } from 'lucide-vue-next'
|
||||
|
||||
const authStr = useAuthStore()
|
||||
|
||||
@ -14,53 +14,49 @@ const {auth} = storeToRefs(authStr)
|
||||
const logout = () => {
|
||||
authStr.set(undefined)
|
||||
router.push('/')
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<RouterLink to="/">
|
||||
<img alt="Conjure logo" class="logo" src="@/assets/logo_conjure_dark.png" width="2228" height="349"/>
|
||||
</RouterLink>
|
||||
<nav v-if="auth">
|
||||
<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 class="flex flex-row justify-between items-center mb-8">
|
||||
<div class="flex flex-row items-center mb-8 gap-10">
|
||||
<RouterLink to="/">
|
||||
<img alt="Conjure logo" class="logo max-w-lg" src="@/assets/logo_conjure_dark.png"/>
|
||||
</RouterLink>
|
||||
<!-- <nav>-->
|
||||
<!-- <RouterLink to="/games">Games</RouterLink>-->
|
||||
<!-- <RouterLink to="/upload">Upload</RouterLink>-->
|
||||
<!-- </nav>-->
|
||||
</div>
|
||||
<button id="logout" class="border-transparent font-bold text-foreground py-2 px-4 border hover:border-primary rounded mb-5"
|
||||
@click="logout()" v-if="auth">
|
||||
<LogOutIcon class="m-2" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<RouterView/>
|
||||
<footer>
|
||||
<button @click="logout()" v-if="auth">Logout</button>
|
||||
<Errors/>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.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;
|
||||
user-drag: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
@ -80,6 +76,11 @@ nav a {
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
#logout {
|
||||
display: inline-block;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
nav a:first-of-type {
|
||||
border: 0;
|
||||
}
|
||||
@ -90,11 +91,6 @@ footer {
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
|
||||
115
src/views/auths/Auth.vue
Normal file
115
src/views/auths/Auth.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<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>
|
||||
.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,70 +1,66 @@
|
||||
<script setup>
|
||||
import {useErrorStore} from '@/stores/errors'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import router from '@/router'
|
||||
import {ref} from 'vue'
|
||||
import Loader from '@/components/Loader.vue'
|
||||
<script setup lang="ts">
|
||||
|
||||
const apiHost = import.meta.env.VITE_CONJUREOS_HOST
|
||||
import { useErrorStore } from '@/stores/errors'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import router from '@/router'
|
||||
import { ref } from 'vue'
|
||||
import Loader from '@/components/Loader.vue'
|
||||
import { AuthService } from '@/services/auth.service'
|
||||
import { AuthDto } from '@/dtos/auth.dto'
|
||||
|
||||
const errorStore = useErrorStore()
|
||||
const authService = new AuthService();
|
||||
const isLoginIn = ref(false)
|
||||
const login = (form) => {
|
||||
const formData = new FormData(form)
|
||||
isLoginIn.value = true
|
||||
fetch(apiHost + 'login', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
const login = async (form: HTMLFormElement) => {
|
||||
const formData = new FormData(form);
|
||||
isLoginIn.value = true;
|
||||
|
||||
const dto = new AuthDto(formData);
|
||||
const response = await authService.login(dto).catch((error) => {
|
||||
isLoginIn.value = false;
|
||||
errorStore.unshift(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
|
||||
errorStore.unshift(error)
|
||||
})
|
||||
|
||||
isLoginIn.value = false;
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
useAuthStore().set(result)
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article>
|
||||
<h1>Login</h1>
|
||||
<form ref="loginForm" enctype="multipart/form-data" @submit.prevent="login($refs.loginForm)">
|
||||
<label for="username">username</label>
|
||||
<form ref="loginForm" enctype="multipart/form-data" @submit.prevent="login($refs.loginForm as HTMLFormElement)">
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
required
|
||||
type="text"
|
||||
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||
name="username"
|
||||
id="username"
|
||||
required
|
||||
type="text"
|
||||
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||
name="username"
|
||||
id="username"
|
||||
/>
|
||||
<label for="password">password</label>
|
||||
<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"
|
||||
name="password"
|
||||
id="password"
|
||||
required
|
||||
type="password"
|
||||
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||
name="password"
|
||||
id="password"
|
||||
/>
|
||||
<button
|
||||
v-if="!isLoginIn"
|
||||
class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded"
|
||||
type="submit"
|
||||
v-if="!isLoginIn"
|
||||
class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded"
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<span class="loader" v-else>
|
||||
<Loader :variant="2"/>
|
||||
<Loader :variant="2" />
|
||||
</span>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
@ -1,37 +1,42 @@
|
||||
<script setup>
|
||||
import {useErrorStore} from '@/stores/errors'
|
||||
import {ref} from "vue";
|
||||
import {usePlayerAuthStore} from '@/stores/player-auth';
|
||||
import router from '@/router';
|
||||
<script setup lang="ts">
|
||||
import { useErrorStore } from '@/stores/errors'
|
||||
import { ref } from 'vue'
|
||||
import { usePlayerAuthStore } from '@/stores/player-auth'
|
||||
import router from '@/router'
|
||||
|
||||
const apiHost = import.meta.env.VITE_CONJUREOS_HOST
|
||||
|
||||
const errorStore = useErrorStore()
|
||||
|
||||
const isLoginIn = ref(false)
|
||||
const signup = (form) => {
|
||||
const formData = new FormData(form);
|
||||
const signup = (form: HTMLFormElement) => {
|
||||
const formData = new FormData(form)
|
||||
isLoginIn.value = true
|
||||
const myHeaders = new Headers()
|
||||
myHeaders.append('API-Version', "1")
|
||||
fetch(apiHost + 'signup', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: myHeaders
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200)
|
||||
return response.text().then(error => {
|
||||
throw new Error(error)
|
||||
}
|
||||
)
|
||||
return response.text()
|
||||
})
|
||||
.then((result) => {
|
||||
isLoginIn.value = false
|
||||
usePlayerAuthStore().set(JSON.parse(result))
|
||||
router.push('/')
|
||||
})
|
||||
.catch((error) => {
|
||||
isLoginIn.value = false
|
||||
errorStore.unshift(error)
|
||||
});
|
||||
.then((response) => {
|
||||
if (response.status !== 200)
|
||||
return response.text().then(error => {
|
||||
throw new Error(error)
|
||||
}
|
||||
)
|
||||
return response.text()
|
||||
})
|
||||
.then((result) => {
|
||||
isLoginIn.value = false
|
||||
console.log(result)
|
||||
usePlayerAuthStore().set(JSON.parse(result))
|
||||
router.push('/')
|
||||
})
|
||||
.catch((error) => {
|
||||
isLoginIn.value = false
|
||||
errorStore.unshift(error)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
@ -39,18 +44,18 @@ const signup = (form) => {
|
||||
<template>
|
||||
<article>
|
||||
<h1>Sign up</h1>
|
||||
<form ref="signupForm" enctype="multipart/form-data" @submit.prevent="signup($refs.signupForm)">
|
||||
<label for="username">username</label>
|
||||
<form ref="signupForm" enctype="multipart/form-data" @submit.prevent="signup($refs.signupForm as HTMLFormElement)">
|
||||
<label for="username">Username</label>
|
||||
<input required type="text"
|
||||
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
|
||||
name="username" id="username"/>
|
||||
<label for="password">password</label>
|
||||
name="username" id="username" />
|
||||
<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"
|
||||
name="password" id="password"/>
|
||||
name="password" id="password" />
|
||||
<button
|
||||
class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded"
|
||||
type="submit">
|
||||
class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded"
|
||||
type="submit">
|
||||
Signup
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@ -5,86 +5,89 @@ import Loader from '@/components/Loader.vue';
|
||||
import {useRoute} from 'vue-router';
|
||||
import {useAuthStore} from '@/stores/auth';
|
||||
import {storeToRefs} from 'pinia';
|
||||
import router from '@/router';
|
||||
|
||||
const errorStore = useErrorStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const {auth} = storeToRefs(authStore)
|
||||
|
||||
const apiHost = import.meta.env.VITE_CONJUREOS_HOST
|
||||
const errorStore = useErrorStore();
|
||||
const authStore = useAuthStore();
|
||||
const { auth } = storeToRefs(authStore);
|
||||
const apiHost = import.meta.env.VITE_CONJUREOS_HOST;
|
||||
|
||||
const route = useRoute();
|
||||
const gameId = ref(route.params.gameId);
|
||||
const game = ref(undefined)
|
||||
const isActivating = ref(false)
|
||||
console.log(route, route.meta)
|
||||
const game = ref(undefined);
|
||||
const isActivating = ref(false);
|
||||
const confirmingDeletion = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
fetch(apiHost + 'games/' + gameId.value, {
|
||||
method: 'GET',
|
||||
headers: { 'API-Version': 1 },
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((result) => {
|
||||
game.value = result
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
errorStore.unshift(error)
|
||||
});
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((result) => {
|
||||
game.value = result;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
errorStore.unshift(error);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function activate() {
|
||||
fetch(apiHost + 'games/' + gameId.value + '/activate', {
|
||||
method: 'POST', headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${auth.value.token}`,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 204)
|
||||
return response.json().then((errorBody) => {
|
||||
throw new Error(errorBody);
|
||||
});
|
||||
return true
|
||||
})
|
||||
.then((result) => {
|
||||
game.value = {...game.value, active: true}
|
||||
isActivating.value = false
|
||||
})
|
||||
.catch((error) => {
|
||||
errorStore.unshift(error)
|
||||
isActivating.value = false
|
||||
});
|
||||
function tryDeleteGame() {
|
||||
console.log("Try")
|
||||
confirmingDeletion.value = true;
|
||||
new Promise((_) => setTimeout(_, 1000)).then(() => {
|
||||
confirmingDeletion.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function deactivate() {
|
||||
isActivating.value = true
|
||||
fetch(apiHost + 'games/' + gameId.value + '/deactivate', {
|
||||
method: 'POST', headers: {
|
||||
function deleteGame() {
|
||||
console.log("Delete")
|
||||
fetch(`${apiHost}games/${gameId.value}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${auth.value.token}`,
|
||||
'API-Version': 1,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 204)
|
||||
return response.json().then((errorBody) => {
|
||||
throw new Error(errorBody);
|
||||
});
|
||||
return true
|
||||
})
|
||||
.then((result) => {
|
||||
game.value = {...game.value, active: false}
|
||||
isActivating.value = false
|
||||
})
|
||||
.catch((error) => {
|
||||
errorStore.unshift(error)
|
||||
isActivating.value = false
|
||||
});
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
alert("Deleted")
|
||||
router.back()
|
||||
}
|
||||
else {
|
||||
alert("Error deleting game")
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
errorStore.unshift(error);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleActivation(state) {
|
||||
isActivating.value = true;
|
||||
fetch(`${apiHost}games/${gameId.value}/${state ? 'activate' : 'deactivate'}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${auth.value.token}`,
|
||||
'API-Version': 1,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 204) return response.json().then((errorBody) => { throw new Error(errorBody); });
|
||||
return true;
|
||||
})
|
||||
.then(() => {
|
||||
game.value = { ...game.value, active: state };
|
||||
isActivating.value = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
errorStore.unshift(error);
|
||||
isActivating.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
@ -92,22 +95,29 @@ function deactivate() {
|
||||
<template>
|
||||
<article>
|
||||
<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"/>
|
||||
<h1>{{ game.game }}</h1>
|
||||
<p>{{ game.description }}</p>
|
||||
<loader :variant="2" v-if="isActivating"></loader>
|
||||
<template v-else>
|
||||
<button v-if="game.active" @click="deactivate()"
|
||||
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">
|
||||
deactivate
|
||||
<button
|
||||
:class="game.active ? 'bg-red-500 text-white hover:bg-red-700' : 'bg-green-500 text-white hover:bg-green-700'"
|
||||
class="font-bold py-2 px-4 my-2 rounded"
|
||||
@click="toggleActivation(!game.active)"
|
||||
:disabled="isActivating"
|
||||
>
|
||||
{{ game.active ? 'Deactivate' : 'Activate' }}
|
||||
</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">
|
||||
activate
|
||||
|
||||
<button
|
||||
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>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
@ -117,24 +127,6 @@ article {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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>
|
||||
@ -1,57 +1,50 @@
|
||||
<script setup>
|
||||
import {onMounted} from 'vue';
|
||||
import {useGamelistStore} from '@/stores/gamelist';
|
||||
import {storeToRefs} from 'pinia';
|
||||
import {useErrorStore} from '@/stores/errors'
|
||||
import Loader from '@/components/Loader.vue';
|
||||
import {RouterLink} from 'vue-router';
|
||||
import { onMounted } from 'vue'
|
||||
import { useGamelistStore } from '@/stores/gamelist'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useErrorStore } from '@/stores/errors'
|
||||
import Loader from '@/components/Loader.vue'
|
||||
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 apiHost = import.meta.env.VITE_CONJUREOS_HOST
|
||||
const gamelistStore = useGamelistStore()
|
||||
const { list: gamelist } = storeToRefs(gamelistStore)
|
||||
const gamesService = new GameService()
|
||||
|
||||
const {list: gamelist} = storeToRefs(gamelistStore)
|
||||
onMounted(async () => {
|
||||
const games = await gamesService.getGames()
|
||||
.catch((error) => {
|
||||
console.error('Error:', error)
|
||||
errorStore.unshift(error)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetch(apiHost + 'games', {
|
||||
method: 'GET',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((result) => {
|
||||
gamelistStore.set(result)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
errorStore.unshift(error)
|
||||
});
|
||||
if (games) {
|
||||
gamelistStore.set(games)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
*/
|
||||
function download(name) {
|
||||
const a = document.createElement('a');
|
||||
a.href = apiHost + 'games/' + name + '/download';
|
||||
document.body.appendChild(a);
|
||||
|
||||
// Programmatically click the anchor element to start the download
|
||||
a.click();
|
||||
|
||||
// Clean up by removing the anchor element
|
||||
document.body.removeChild(a);
|
||||
const a = document.createElement('a')
|
||||
a.href = apiHost + 'games/' + name + '/download'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
function downloadAll() {
|
||||
const a = document.createElement('a');
|
||||
a.href = apiHost + 'games/download';
|
||||
document.body.appendChild(a);
|
||||
const a = document.createElement('a')
|
||||
a.href = apiHost + 'games/download'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
// Programmatically click the anchor element to start the download
|
||||
a.click();
|
||||
|
||||
// Clean up by removing the anchor element
|
||||
document.body.removeChild(a);
|
||||
function newGame() {
|
||||
router.push("/upload")
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -59,63 +52,53 @@ function downloadAll() {
|
||||
<article>
|
||||
<loader v-if="gamelist === undefined"></loader>
|
||||
<template v-else>
|
||||
<header>
|
||||
<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"
|
||||
<header class="flex flex-row justify-between items-center">
|
||||
<h1 class="text-foreground">My Games</h1>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
class="bg-transparent font-bold text-foreground py-2 px-4 border border-primary rounded hover:bg-primary"
|
||||
@click="downloadAll()"
|
||||
>
|
||||
Download all
|
||||
</button>
|
||||
>
|
||||
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>
|
||||
</div>
|
||||
</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>
|
||||
<h2>{{ item.game }}</h2>
|
||||
<div>
|
||||
<RouterLink :to="'/games/' + item.id"
|
||||
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">
|
||||
open
|
||||
</RouterLink>
|
||||
<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"
|
||||
type="button" @click="download(item.id + '.conj')">Download
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
<h2 class="font-bold">{{ item.game }}</h2>
|
||||
<p class="text-primary">{{ item.description }}</p>
|
||||
<i v-if="item.active" class="text-green-500 pt-3">Active</i>
|
||||
</div>
|
||||
</RouterLink>
|
||||
<button
|
||||
class="p-2 rounded-full hover:bg-gray-200 text-green-500 hover:text-green-700"
|
||||
@click="download(item.id)"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped>
|
||||
.game-item:hover {
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
article {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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>
|
||||
|
||||
221
src/views/games/ManualUploadView.vue
Normal file
221
src/views/games/ManualUploadView.vue
Normal file
@ -0,0 +1,221 @@
|
||||
<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() {
|
||||
const form = this.$refs.uploadForm
|
||||
const formData = new FormData(form)
|
||||
console.log("Upload " + JSON.stringify(authStr.getAuth()))
|
||||
|
||||
fetch(apiHost + 'games', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
Authorization: authStr.getAuth()
|
||||
Authorization: `Bearer ${authStr.getAuth().token}`,
|
||||
'API-Version': 1
|
||||
}
|
||||
})
|
||||
.then((response) => response.text())
|
||||
@ -44,7 +46,7 @@ export default {
|
||||
<template>
|
||||
<article>
|
||||
<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">-->
|
||||
<!-- <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"-->
|
||||
@ -57,6 +59,7 @@ export default {
|
||||
name="file"
|
||||
id="file"
|
||||
accept=".conj"
|
||||
class="cursor-pointer"
|
||||
@change="filesChanges"
|
||||
/>
|
||||
<label>
|
||||
@ -65,8 +68,13 @@ export default {
|
||||
<em class="file-name" v-if="!!selectedFiles?.length">{{ selectedFiles[0].name }}</em>
|
||||
</label>
|
||||
</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
|
||||
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"
|
||||
>
|
||||
Upload
|
||||
@ -76,11 +84,9 @@ export default {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
form {
|
||||
padding: 1rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.file-upload-wrapper {
|
||||
|
||||
@ -21,9 +21,12 @@ const submit = (form) => {
|
||||
const formData = new FormData(form)
|
||||
formData.set('token', route.query.token.toString())
|
||||
isLoginIn.value = true
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append("API-Version", 1);
|
||||
fetch(apiHost + route.query.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
body: formData,
|
||||
headers: myHeaders,
|
||||
})
|
||||
.then((response) => {
|
||||
isLoginIn.value = false
|
||||
|
||||
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": true,
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@ -1,25 +1,26 @@
|
||||
import {fileURLToPath, URL} from 'node:url'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import {defineConfig} from 'vite'
|
||||
import { defineConfig } from 'vite'
|
||||
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
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
build: {
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
plugins: [
|
||||
vue()
|
||||
],
|
||||
build: {
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: false
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user