continuer cleaner page admin

This commit is contained in:
Trit0 2025-03-10 21:27:03 -04:00
parent 5fd32ef333
commit 4a26259891
33 changed files with 3575 additions and 5174 deletions

View File

@ -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'
}
}

20
eslint.config.cjs Normal file
View File

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

View File

@ -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>

7523
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
"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 . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
@ -15,26 +15,33 @@
"@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",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-router": "^4.2.4" "vue-router": "^4.2.4"
}, },
"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",
"typescript": "^5.7.3",
"vite": "^4.4.9", "vite": "^4.4.9",
"vitest": "^0.34.4" "vitest": "^0.34.4",
"vue-tsc": "^2.2.2"
} }
} }

View File

@ -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 */ /* Example CSS for headings with additional styles */
h1 { h1 {

9
src/dtos/auth.dto.ts Normal file
View 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;
}
}

6
src/interfaces/auth.ts Normal file
View File

@ -0,0 +1,6 @@
export interface AuthResponse {
id: number;
roles: string[];
token: string;
username: string;
}

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

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

View File

@ -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

110
src/router/index.ts Normal file
View File

@ -0,0 +1,110 @@
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: '/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

View 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);
}
}

View File

@ -0,0 +1,44 @@
export class BaseService {
protected apiUrl: string | undefined = import.meta.env.VITE_CONJUREOS_HOST
protected baseHeaders: Record<string, string> = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'API-Version': '1'
}
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.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.baseHeaders
}
});
}
}

View File

@ -0,0 +1,53 @@
import { BaseService } from './base-service'
import { Game } from '../interfaces/game'
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: Game) : Promise<void> {
return this.post<any>(`games`, null).then();
}
public async update(game: Game) : Promise<void> {
return this.put<any>(`games`, null).then();
}
public async activate(gameId: string): Promise<void> {
return this.post<any>(`games/${gameId}/activate`, null).then();
}
public async deactivate(gameId: string): Promise<void> {
return this.post<any>(`games/${gameId}/deactivate`, null).then();
}
public async download(gameId: string): Promise<void> {
return this.get<any>(`games/${gameId}/download`).then();
}
public async downloadAll(): Promise<void> {
return this.get<any>(`games/download`).then();
}
public async patch(): Promise<void> {
return this.put<any>(`games/patch`, null).then();
}
public async minor(): Promise<void> {
return this.put<any>(`games/minor`, null).then();
}
public async major(): Promise<void> {
return this.put<any>(`games/major`, null).then();
}
public async metadata(): Promise<void> {
return this.post<any>(`games/metadata`, null).then();
}
}

6
src/shims-vue.d.ts vendored Normal file
View 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;
}

View File

@ -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
View 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 }
})

View File

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

View File

@ -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
View 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 }
})

View File

@ -1,30 +1,28 @@
<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 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"> 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 />
</footer> </footer>
</template> </template>

View File

@ -1,11 +1,11 @@
<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 router from '@/router';
import Errors from '@/components/Errors.vue' import Errors from '@/components/Errors.vue'
import {ref} from 'vue'; import { LogOutIcon } from 'lucide-vue-next'
const authStr = useAuthStore() const authStr = useAuthStore()
@ -14,53 +14,48 @@ const {auth} = storeToRefs(authStr)
const logout = () => { const logout = () => {
authStr.set(undefined) authStr.set(undefined)
router.push('/') router.push('/')
location.reload()
} }
</script> </script>
<template> <template>
<header> <header class="flex flex-row justify-between items-center mb-8">
<RouterLink to="/"> <div class="flex flex-row items-center mb-8 gap-10">
<img alt="Conjure logo" class="logo" src="@/assets/logo_conjure_dark.png" width="2228" height="349"/> <RouterLink to="/">
</RouterLink> <img alt="Conjure logo" class="logo max-w-lg" src="@/assets/logo_conjure_dark.png"/>
<nav v-if="auth"> </RouterLink>
<RouterLink to="/games">Games</RouterLink> <nav>
<RouterLink to="/upload">Upload</RouterLink> <RouterLink to="/games">Games</RouterLink>
</nav> <RouterLink to="/upload">Upload</RouterLink>
<nav v-else> </nav>
<RouterLink to="/login">Login</RouterLink> </div>
<RouterLink to="/sign-up">Sign up</RouterLink> <button id="logout" class="p-2 rounded-full border hover:border-gray-300" @click="logout()" v-if="auth">
</nav> <LogOutIcon class="m-2" />
</button>
</header> </header>
<RouterView/> <RouterView/>
<footer> <footer>
<button @click="logout()" v-if="auth">Logout</button>
<Errors/> <Errors/>
</footer> </footer>
</template> </template>
<style scoped> <style scoped>
.logo {
user-drag: none;
-webkit-user-drag: none;
}
header { header {
line-height: 1.5; line-height: 1.5;
max-height: 100dvh; max-height: 100dvh;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.logo { .logo {
display: block; display: block;
margin: 0 auto 2rem; margin: 0 auto 2rem;
user-drag: none;
-webkit-user-drag: none;
} }
nav { nav {
width: 100%;
font-size: 12px; font-size: 12px;
text-align: center; text-align: center;
margin-top: 2rem; margin-top: 2rem;
@ -80,6 +75,11 @@ nav a {
border-left: 1px solid var(--color-border); border-left: 1px solid var(--color-border);
} }
#logout {
display: inline-block;
padding: 0 1rem;
}
nav a:first-of-type { nav a:first-of-type {
border: 0; border: 0;
} }
@ -90,11 +90,6 @@ footer {
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo { .logo {
margin: 0 2rem 0 0; margin: 0 2rem 0 0;

115
src/views/auths/Auth.vue Normal file
View 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>

View File

@ -1,74 +1,66 @@
<script setup> <script setup lang="ts">
import {useErrorStore} from '@/stores/errors'
import {useAuthStore} from '@/stores/auth'
import router from '@/router'
import {ref} from 'vue'
import Loader from '@/components/Loader.vue'
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 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;
const myHeaders = new Headers(); const dto = new AuthDto(formData);
myHeaders.append("API-Version", 1); const response = await authService.login(dto).catch((error) => {
fetch(apiHost + 'login', { isLoginIn.value = false;
method: 'POST', errorStore.unshift(error);
body: formData,
headers: myHeaders
}) })
.then((response) => {
isLoginIn.value = false isLoginIn.value = false;
if (response.status !== 200) if (!response) {
return response.text().then(error => { return;
throw new Error(error) }
}
) const result = await response.json();
return response.text() useAuthStore().set(result)
}) router.push('/')
.then((result) => {
useAuthStore().set(JSON.parse(result))
router.push('/')
})
.catch((error) => {
isLoginIn.value = false
errorStore.unshift(error)
})
} }
</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 ref="loginForm" enctype="multipart/form-data" @submit.prevent="login($refs.loginForm as HTMLFormElement)">
<label for="username">username</label> <label for="username">Username</label>
<input <input
required required
type="text" type="text"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
name="username" name="username"
id="username" id="username"
/> />
<label for="password">password</label> <label for="password">Password</label>
<input <input
required required
type="password" type="password"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
name="password" name="password"
id="password" id="password"
/> />
<button <button
v-if="!isLoginIn" v-if="!isLoginIn"
class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded" class="bg-transparent text-primary font-semibold hover:text-white py-2 px-4 border border-gray-500 hover:border-transparent rounded"
type="submit" type="submit"
> >
Login Login
</button> </button>
<span class="loader" v-else> <span class="loader" v-else>
<Loader :variant="2"/> <Loader :variant="2" />
</span> </span>
</form> </form>
</article> </article>

View File

@ -1,41 +1,42 @@
<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(); const myHeaders = new Headers()
myHeaders.append("API-Version", 1); myHeaders.append('API-Version', "1")
fetch(apiHost + 'signup', { fetch(apiHost + 'signup', {
method: 'POST', method: 'POST',
body: formData, body: formData,
headers: myHeaders 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
usePlayerAuthStore().set(JSON.parse(result)) console.log(result)
router.push('/') usePlayerAuthStore().set(JSON.parse(result))
}) router.push('/')
.catch((error) => { })
isLoginIn.value = false .catch((error) => {
errorStore.unshift(error) isLoginIn.value = false
}); errorStore.unshift(error)
})
} }
</script> </script>
@ -43,18 +44,18 @@ const signup = (form) => {
<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 ref="signupForm" enctype="multipart/form-data" @submit.prevent="signup($refs.signupForm as HTMLFormElement)">
<label for="username">username</label> <label for="username">Username</label>
<input required type="text" <input required type="text"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent" class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
name="username" id="username"/> name="username" id="username" />
<label for="password">password</label> <label for="password">Password</label>
<input required type="password" <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>

View File

@ -6,90 +6,53 @@ import {useRoute} from 'vue-router';
import {useAuthStore} from '@/stores/auth'; import {useAuthStore} from '@/stores/auth';
import {storeToRefs} from 'pinia'; import {storeToRefs} from 'pinia';
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)
onMounted(() => { onMounted(() => {
const myHeaders = new Headers();
myHeaders.append("API-Version", 1);
fetch(apiHost + 'games/' + gameId.value, { fetch(apiHost + 'games/' + gameId.value, {
method: 'GET', method: 'GET',
headers: myHeaders 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 toggleActivation(state) {
* isActivating.value = true;
*/ fetch(`${apiHost}games/${gameId.value}/${state ? 'activate' : 'deactivate'}`, {
function activate() { method: 'POST',
fetch(apiHost + 'games/' + gameId.value + '/activate', { 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, 'API-Version': 1,
}, },
}) })
.then((response) => { .then((response) => {
if (response.status !== 204) if (response.status !== 204) return response.json().then((errorBody) => { throw new Error(errorBody); });
return response.json().then((errorBody) => { return true;
throw new Error(errorBody); })
}); .then(() => {
return true game.value = { ...game.value, active: state };
}) isActivating.value = false;
.then((result) => { })
game.value = {...game.value, active: true} .catch((error) => {
isActivating.value = false errorStore.unshift(error);
}) isActivating.value = false;
.catch((error) => { });
errorStore.unshift(error)
isActivating.value = false
});
}
/**
*
*/
function deactivate() {
isActivating.value = true
fetch(apiHost + 'games/' + gameId.value + '/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((result) => {
game.value = {...game.value, active: false}
isActivating.value = false
})
.catch((error) => {
errorStore.unshift(error)
isActivating.value = false
});
} }
</script> </script>
@ -97,22 +60,22 @@ function deactivate() {
<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="game.active ? 'bg-red-500 text-white hover:bg-red-700' : 'bg-green-500 text-white hover:bg-green-700'"
deactivate class="font-bold py-2 px-4 my-2 rounded"
</button> @click="toggleActivation(!game.active)"
<button v-else @click="activate()" :disabled="isActivating"
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 {{ game.active ? 'Deactivate' : 'Activate' }}
</button> </button>
</template> </template>
</template> </div>
</article> </article>
</template> </template>
@ -122,24 +85,6 @@ 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>

View File

@ -1,60 +1,45 @@
<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'
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 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(() => { if (games) {
const myHeaders = new Headers(); gamelistStore.set(games)
myHeaders.append("API-Version", 1); }
fetch(apiHost + 'games', {
method: 'GET',
headers: myHeaders
})
.then((response) => response.json())
.then((result) => {
gamelistStore.set(result)
})
.catch((error) => {
console.error('Error:', error);
errorStore.unshift(error)
});
}) })
/**
* @param {string} name
*/
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()
// 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);
} }
</script> </script>
@ -62,63 +47,45 @@ 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>
<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>
</header> </header>
<ul> <ul class="flex flex-col gap-4">
<li class="game rounded-lg p-4" v-for="(item) in gamelist" :key="item.id"> <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">
<img v-if="item.thumbnail" :src="'data:image/png;base64,'+item.thumbnail" alt="thumbnail"/> <RouterLink :to="'/games/' + item.id" class="flex-grow flex items-center gap-4 p-2 rounded">
<span v-else></span> <img v-if="item.thumbnail" :src="'data:image/png;base64,' + item.thumbnail" alt="thumbnail" class="h-16" />
<div class="flex flex-col"> <div>
<p>{{ item.description }}</p> <h2 class="font-bold">{{ item.game }}</h2>
<i class="ml-auto" v-if="item.active">active</i> <p class="text-primary">{{ item.description }}</p>
</div> <i v-if="item.active" class="text-green-500 pt-3">Active</i>
<h2>{{ item.game }}</h2> </div>
<div> </RouterLink>
<RouterLink :to="'/games/' + item.id" <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"
open @click="download(item.id)"
</RouterLink> >
<button <DownloadIcon />
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" </button>
type="button" @click="download(item.id + '.conj')">Download
</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>

View File

@ -16,7 +16,7 @@ export default {
submitForm() { submitForm() {
const form = this.$refs.uploadForm const form = this.$refs.uploadForm
const formData = new FormData(form) const formData = new FormData(form)
console.log("Upload") console.log("Upload " + JSON.stringify(authStr.getAuth()))
fetch(apiHost + 'games', { fetch(apiHost + 'games', {
method: 'POST', method: 'POST',

35
tsconfig.json Normal file
View 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
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": [
"vite.config.ts"
]
}

View File

@ -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' 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: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
} }
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
}
}
}) })