Compare commits

..

5 Commits

Author SHA1 Message Date
Trit0
6a994cee31 wip new thing 2026-02-15 16:09:25 -05:00
Trit0
fddcd1fecd wip 2026-02-15 10:24:30 -05:00
Trit0
174e7e57eb manual upload works in happy path + delete game 2025-03-14 15:13:03 -04:00
Trit0
4a26259891 continuer cleaner page admin 2025-03-10 21:27:03 -04:00
Trit0
5fd32ef333 commencer cleanup 2025-02-13 15:20:40 -05:00
67 changed files with 5480 additions and 5985 deletions

2
.env.sample Normal file
View File

@ -0,0 +1,2 @@
VITE_CONJUREOS_HOST=
VITE_CONJUREOS_MQTT=

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

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ coverage
*.njsproj
*.sln
*.sw?
.env

View File

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

View File

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

View File

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

44
eslint.config.mjs Normal file
View File

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

View File

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

8503
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,37 +4,48 @@
"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",
"lint": "eslint . --fix",
"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",
"typescript-eslint": "^8.55.0",
"vite": "^5.4.14",
"vitest": "^3.0.8",
"vue-tsc": "^2.2.2"
}
}

View File

@ -1,11 +1,11 @@
<script setup>
import { RouterLink, RouterView, useRoute } 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 router from '@/router'
import Errors from '@/components/Errors.vue'
import {ref} from 'vue';
import { ref } from 'vue'
const authStr = useAuthStore()

View File

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

View File

@ -1,27 +1,15 @@
@import 'base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
gap: 1rem;
font-weight: normal;
}
a.router-link-active {
color: var(--vt-c-munsell) !important;
}
a {
text-decoration: none;
color: var(--vt-c-silver);
transition: 0.4s;
}
@media (hover: hover) {
/*a:hover,*/
/*button:hover {*/
@ -29,24 +17,10 @@ a {
/*}*/
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}
/* Example CSS for headings with additional styles */
h1 {
font-size: 2rem;
line-height: 4rem
line-height: 4rem;
}
h2 {

View File

@ -0,0 +1,71 @@
<template>
<div>
<v-menu
v-model="menu"
:close-on-content-click="false"
:nudge-right="40"
transition="scale-transition"
offset-y
min-width="290px"
>
<template v-slot:activator="{ props }">
<v-text-field
v-bind="props"
:value="dateFormatted"
variant="outlined"
append-inner-icon="mdi-calendar"
@change="onChange"
@input="updateDate"
></v-text-field>
</template>
<v-date-picker :model-value="getDate" @update:modelValue="updateDate"></v-date-picker>
</v-menu>
</div>
</template>
<script>
export default {
props: {
/**
* Date on ISO format to be edited.
* @model
*/
value: {
type: String,
default() {
return ''
}
}
},
data() {
return {
menu: false
}
},
computed: {
dateFormatted() {
return this.input ? new Date(this.input) : ''
},
getDate() {
const date = this.input ? new Date(this.input) : new Date()
return [date]
}
},
methods: {
onChange(val) {
console.log(val)
},
updateDate(val) {
this.menu = false
console.log(val)
this.input = val
}
}
}
</script>
<style scoped>
.v-text-field input {
background-color: transparent !important;
}
</style>

View File

@ -1,6 +1,6 @@
<script setup>
import { useErrorStore } from '@/stores/errors'
import {storeToRefs} from "pinia";
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
let isOpen = ref(false)

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

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,6 @@ import SupportIcon from './icons/IconSupport.vue'
<template>
<article>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
@ -53,8 +52,8 @@ import SupportIcon from './icons/IconSupport.vue'
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>.
If you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
@ -71,8 +70,8 @@ import SupportIcon from './icons/IconSupport.vue'
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also subscribe to
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
the official
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and
follow the official
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
twitter account for latest news in the Vue world.
</WelcomeItem>
@ -83,8 +82,8 @@ import SupportIcon from './icons/IconSupport.vue'
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
As an independent project, Vue relies on community backing for its sustainability. You can
help us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</article>

View File

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

9
src/dtos/auth.dto.ts Normal file
View File

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

View File

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

View File

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

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

@ -0,0 +1,11 @@
export interface Role {
id: number
label: string
}
export interface AuthResponse {
id: number
role: Role
token: string
email: string
}

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

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

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

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

View File

@ -1,14 +0,0 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

27
src/main.ts Normal file
View File

@ -0,0 +1,27 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
// Vuetify
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import DateFnsAdapter from '@date-io/date-fns'
import App from './App.vue'
import router from './router'
const vuetify = createVuetify({
components,
directives,
theme: false
})
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(vuetify)
app.mount('#app')

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

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

@ -0,0 +1,132 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
meta: { requiresAuth: true }
},
{
path: '',
name: 'member',
component: () => import('../views/Member.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
redirect: '/games'
},
{
path: '/games',
name: 'games',
component: () => import('../views/games/GamesView.vue')
},
{
path: '/upload',
name: 'upload',
component: () => import('../views/games/UploadView.vue')
},
{
path: '/manual-upload',
name: 'manual-upload',
component: () => import('../views/games/ManualUploadView.vue')
},
{
path: '/games/:gameId',
name: 'game',
component: () => import('../views/games/GameView.vue')
},
{
path: '/users',
name: 'users',
component: () => import('../views/users/UsersView.vue'),
meta: { requiresAdmin: true }
}
]
},
{
path: '',
name: 'auth',
component: () => import('../views/auths/Auth.vue'),
meta: { requiresAuth: false },
children: [
{
path: '/login',
name: 'login',
component: () => import('../views/auths/Login.vue')
},
{
path: '/sign-up',
name: 'sign-up',
component: () => import('../views/auths/SignUp.vue')
}
]
},
{
path: '',
name: 'limited',
component: () => import('../views/Limited.vue'),
children: [
{
path: '/close',
name: 'close',
component: () => import('../views/players/Close.vue')
},
{
path: '/action',
name: 'action',
component: () => import('../views/players/QrAction.vue')
},
{
path: '/:catchAll(.*)',
name: 'shit',
component: () => import('../views/NotFound.vue')
}
]
}
]
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
const isLoggedIn = authStore.isAuth()
const userRole = authStore.getAuth()?.role?.label
// Handle routes that require admin access
if (to.meta.requiresAdmin) {
if (isLoggedIn && userRole === 'Admin') {
next() // Allow access
} else {
next('/games') // Redirect non-admins
}
return
}
// Handle general authentication requirements
if (to.meta.requiresAuth === undefined) {
next()
return
}
if (isLoggedIn === to.meta.requiresAuth) {
// If the condition is met, allow access to the route
next()
} else if (to.meta.requiresAuth) {
console.error('sneaky')
// If the condition is not met, redirect to another route
next('/login') // Redirect to the login page
} else {
console.error('sneaky')
next('/')
}
})
export default router

View File

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

View File

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

View File

@ -0,0 +1,62 @@
import { BaseService } from './base-service'
import { Game } from '@/interfaces/game'
import { ValidateMetadataDto } from '@/dtos/validate-metadata.dto'
import { useAuthStore } from '@/stores/auth'
import { AuthDto } from '@/dtos/auth.dto'
export class GameService extends BaseService {
public getGames(): Promise<Game[]> {
return this.get<Game[]>('games')
}
public getGame(gameId: string): Promise<Game> {
return this.get<Game>(`games/${gameId}`)
}
public async upload(game: FormData): Promise<Response> {
const authStr = useAuthStore()
return this.postForm(`games`, game, {
Authorization: `Bearer ${authStr.getAuth()?.token}`
})
}
public async update(game: Game): Promise<void> {
await this.put<any>(`games`, null)
}
public async activate(gameId: string): Promise<void> {
await this.post<any>(`games/${gameId}/activate`, null)
}
public async deactivate(gameId: string): Promise<void> {
await this.post<any>(`games/${gameId}/deactivate`, null)
}
public async download(gameId: string): Promise<void> {
await this.get<any>(`games/${gameId}/download`)
}
public async downloadAll(): Promise<void> {
await this.get<any>(`games/download`)
}
public async patch(): Promise<void> {
await this.put<any>(`games/patch`, null)
}
public async minor(): Promise<void> {
await this.put<any>(`games/minor`, null)
}
public async major(): Promise<void> {
await this.put<any>(`games/major`, null)
}
public async metadata(dto: ValidateMetadataDto): Promise<Response> {
return this.post(`games/metadata`, dto)
}
public async deleteGame(gameId: string): Promise<void> {
await this.delete(`games/${gameId}`)
}
}

View File

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

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

25
src/stores/auth.ts Normal file
View File

@ -0,0 +1,25 @@
import { defineStore } from 'pinia'
import { Ref, ref } from 'vue'
import { AuthResponse } from '@/interfaces/auth'
const key = 'AUTH'
export const useAuthStore = defineStore('auth', () => {
const localStorageAuth = localStorage.getItem(key)
const auth: Ref<AuthResponse | null> = ref(localStorageAuth ? JSON.parse(localStorageAuth) : null)
function set(_auth: AuthResponse) {
auth.value = _auth
if (_auth) localStorage.setItem(key, JSON.stringify(_auth))
else localStorage.removeItem(key)
}
function isAuth() {
return !!auth.value?.token
}
function getAuth() {
return auth.value
}
return { auth, getAuth, set, isAuth }
})

View File

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

18
src/stores/errors.ts Normal file
View File

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

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

25
src/stores/player-auth.ts Normal file
View File

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

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

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

8
src/utils/blob.util.ts Normal file
View File

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

51
src/utils/conj.util.ts Normal file
View File

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

17
src/utils/file-saver.ts Normal file
View 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
View File

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

163
src/utils/zip.util.ts Normal file
View File

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

View File

@ -1,27 +1,33 @@
<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)
</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 />
<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">
<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 />
@ -29,7 +35,6 @@ const {auth} = storeToRefs(authStr)
</template>
<style scoped>
.logo {
user-drag: none;
-webkit-user-drag: none;

View File

@ -1,118 +1,25 @@
<script setup>
import {RouterLink, RouterView, useRoute} from 'vue-router'
import {storeToRefs} from 'pinia';
import {useAuthStore} from '@/stores/auth';
import router from '@/router';
import { RouterView } from 'vue-router'
import Sidebar from '@/components/Sidebar.vue'
import Header from '@/components/Header.vue'
import Errors from '@/components/Errors.vue'
import {ref} from 'vue';
const authStr = useAuthStore()
const {auth} = storeToRefs(authStr)
const logout = () => {
authStr.set(undefined)
router.push('/')
}
</script>
<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>
<div class="flex h-screen w-screen bg-muted/40">
<Sidebar />
<div class="flex flex-col flex-1">
<Header />
<main class="flex-1 p-8 overflow-y-auto">
<RouterView />
<footer>
<button @click="logout()" v-if="auth">Logout</button>
</main>
<footer class="p-4">
<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;
}
}
/* Scoped styles can remain if there are any specific to Member.vue layout */
</style>

View File

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

129
src/views/auths/Auth.vue Normal file
View File

@ -0,0 +1,129 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import Errors from '@/components/Errors.vue'
</script>
<template>
<div id="auth">
<div id="auth-content">
<header>
<RouterLink to="/">
<img
alt="Conjure logo"
class="logo"
src="@/assets/logo_conjure_dark.png"
width="2228"
height="349"
/>
</RouterLink>
<nav>
<RouterLink to="/login">Login</RouterLink>
<RouterLink to="/sign-up">Sign up</RouterLink>
</nav>
</header>
<RouterView />
<footer>
<Errors />
</footer>
</div>
</div>
</template>
<style scoped>
#auth {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
gap: 1rem;
font-weight: normal;
}
.logo {
user-drag: none;
-webkit-user-drag: none;
}
header {
line-height: 1.5;
max-height: 100dvh;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
footer {
display: flex;
gap: 1rem;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
#auth {
min-height: 100vh;
display: flex;
place-items: center;
}
#auth-content {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}
</style>

View File

@ -1,54 +1,53 @@
<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 { 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 login = async (form: HTMLFormElement) => {
const formData = new FormData(form)
isLoginIn.value = true
fetch(apiHost + 'login', {
method: 'POST',
body: formData
})
.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) => {
const dto = new AuthDto(formData)
const response = await authService.login(dto).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="email">Email</label>
<input
required
type="text"
type="email"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
name="username"
id="username"
name="email"
id="email"
/>
<label for="password">password</label>
<label for="password">Password</label>
<input
required
type="password"

View File

@ -1,56 +1,72 @@
<script setup>
<script setup lang="ts">
import { useErrorStore } from '@/stores/errors'
import {ref} from "vue";
import {usePlayerAuthStore} from '@/stores/player-auth';
import router from '@/router';
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 => {
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>
<template>
<article>
<h1>Sign up</h1>
<form ref="signupForm" enctype="multipart/form-data" @submit.prevent="signup($refs.signupForm)">
<label for="username">username</label>
<input required type="text"
<form
ref="signupForm"
enctype="multipart/form-data"
@submit.prevent="signup($refs.signupForm as HTMLFormElement)"
>
<label for="email">Email</label>
<input
required
type="email"
class="border border-primary rounded-lg px-3 py-2 bg-transparent focus:outline-none focus:border-accent"
name="username" id="username"/>
<label for="password">password</label>
<input required type="password"
name="email"
id="email"
/>
<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">
type="submit"
>
Signup
</button>
</form>

View File

@ -1,140 +1,135 @@
<script setup>
import {onMounted, ref} from 'vue';
import { onMounted, ref } from 'vue'
import { useErrorStore } from '@/stores/errors'
import Loader from '@/components/Loader.vue';
import {useRoute} from 'vue-router';
import {useAuthStore} from '@/stores/auth';
import {storeToRefs} from 'pinia';
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 route = useRoute();
const gameId = ref(route.params.gameId);
const route = useRoute()
const gameId = ref(route.params.gameId)
const game = ref(undefined)
const isActivating = ref(false)
console.log(route, route.meta)
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);
console.error('Error:', error)
errorStore.unshift(error)
});
})
})
/**
*
*/
function activate() {
fetch(apiHost + 'games/' + gameId.value + '/activate', {
method: 'POST', headers: {
function tryDeleteGame() {
console.log('Try')
confirmingDeletion.value = true
new Promise((_) => setTimeout(_, 1000)).then(() => {
confirmingDeletion.value = false
})
}
function deleteGame() {
console.log('Delete')
fetch(`${apiHost}games/${gameId.value}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${auth.value.token}`,
},
Authorization: `Bearer ${auth.value.token}`,
'API-Version': 1
}
})
.then((response) => {
if (response.status !== 204)
return response.json().then((errorBody) => {
throw new Error(errorBody);
});
if (response.ok) {
alert('Deleted')
router.back()
} else {
alert('Error deleting game')
}
return true
})
.then((result) => {
game.value = {...game.value, active: true}
isActivating.value = false
})
.catch((error) => {
errorStore.unshift(error)
isActivating.value = false
});
})
}
/**
*
*/
function deactivate() {
function toggleActivation(state) {
isActivating.value = true
fetch(apiHost + 'games/' + gameId.value + '/deactivate', {
method: 'POST', headers: {
fetch(`${apiHost}games/${gameId.value}/${state ? 'activate' : 'deactivate'}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${auth.value.token}`,
},
Authorization: `Bearer ${auth.value.token}`,
'API-Version': 1
}
})
.then((response) => {
if (response.status !== 204)
return response.json().then((errorBody) => {
throw new Error(errorBody);
});
throw new Error(errorBody)
})
return true
})
.then((result) => {
game.value = {...game.value, active: false}
.then(() => {
game.value = { ...game.value, active: state }
isActivating.value = false
})
.catch((error) => {
errorStore.unshift(error)
isActivating.value = false
});
})
}
</script>
<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>
<style scoped lang="scss">
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>

View File

@ -1,57 +1,49 @@
<script setup>
import {onMounted} from 'vue';
import {useGamelistStore} from '@/stores/gamelist';
import {storeToRefs} from 'pinia';
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 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()
onMounted(() => {
fetch(apiHost + 'games', {
method: 'GET',
})
.then((response) => response.json())
.then((result) => {
gamelistStore.set(result)
})
.catch((error) => {
console.error('Error:', error);
onMounted(async () => {
const games = await gamesService.getGames().catch((error) => {
console.error('Error:', error)
errorStore.unshift(error)
});
})
/**
* @param {string} name
*/
if (games) {
gamelistStore.set(games)
}
})
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 +51,65 @@ function downloadAll() {
<article>
<loader v-if="gamelist === undefined"></loader>
<template v-else>
<header>
<header class="flex flex-row justify-between items-center">
<h1 class="text-foreground">My Games</h1>
<div class="flex gap-3">
<button
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()"
>
Download all
Download All
</button>
<button
class="bg-transparent font-bold text-foreground py-2 px-4 border border-primary rounded hover:bg-primary"
@click="newGame()"
>
New Game
</button>
</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>
</header>
<ul class="flex flex-col gap-4">
<li
v-for="item in gamelist"
:key="item.id"
class="game-item border border-gray-300 rounded-lg p-4 flex items-center gap-4 hover:border-2"
>
<RouterLink
:to="'/games/' + item.id"
class="flex-grow flex items-center gap-4 p-2 rounded"
>
<img
v-if="item.thumbnail"
:src="'data:image/png;base64,' + item.thumbnail"
alt="thumbnail"
class="h-16"
/>
<div>
<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
<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="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
class="p-2 rounded-full hover:bg-gray-200 text-green-500 hover:text-green-700"
@click="download(item.id)"
>
<DownloadIcon />
</button>
</div>
</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>

View File

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

View File

@ -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,12 @@ 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 +64,7 @@ export default {
name="file"
id="file"
accept=".conj"
class="cursor-pointer"
@change="filesChanges"
/>
<label>
@ -65,8 +73,15 @@ 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
@ -78,9 +93,6 @@ export default {
<style scoped lang="scss">
form {
padding: 1rem 0;
display: flex;
flex-direction: column;
gap: 2rem;
}
.file-upload-wrapper {

View File

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

View File

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

View File

@ -1,37 +1,38 @@
<script setup>
import { ref } from 'vue'
import {useRoute} from 'vue-router';
import Loader from '@/components/Loader.vue';
import { useRoute } from 'vue-router'
import Loader from '@/components/Loader.vue'
import router from '@/router'
import { useErrorStore } from '@/stores/errors'
const errorStore = useErrorStore()
const apiHost = import.meta.env.VITE_CONJUREOS_HOST
const isLoginIn = ref(false)
const route = useRoute();
const route = useRoute()
if (!route.query.token || !route.query.action) {
router.push("/close")
router.push('/close')
}
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
if (response.status !== 200)
return response.text().then(error => {
return response.text().then((error) => {
throw new Error(error)
}
)
})
return response.text()
})
.then((result) => {

View File

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

35
tsconfig.json Normal file
View File

@ -0,0 +1,35 @@
{
"compilerOptions": {
"target": "ESNext",
"types": ["vite/client"],
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": false,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": [
"ESNext",
"DOM"
],
"skipLibCheck": true,
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

11
tsconfig.node.json Normal file
View File

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

View File

@ -3,23 +3,24 @@ import {fileURLToPath, URL} from 'node:url'
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(),
vue()
],
build: {
terserOptions: {
compress: {
drop_console: false,
},
},
drop_console: false
}
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
'@': fileURLToPath(new URL('./src', import.meta.url)),
}
}
})