Move the webapp into its own folder
This commit is contained in:
51
app/Dockerfile
Normal file
51
app/Dockerfile
Normal file
@@ -0,0 +1,51 @@
|
||||
# FROM ubuntu:24.04
|
||||
|
||||
# ADD ./loader/loader-crm /etc/cron.d/loader-crm
|
||||
# RUN chmod 0644 /etc/cron/loader-crm
|
||||
# RUN touch /var/log/chron.log
|
||||
|
||||
# RUN apt-get update
|
||||
# RUN apt-get -y install cron
|
||||
# RUN systemctl enable cron
|
||||
|
||||
# Setup the loader
|
||||
FROM python:3.12 AS loader
|
||||
COPY ./loader /srv/cprush-stats
|
||||
WORKDIR /srv/cprush-stats
|
||||
RUN python -m pip install --upgrade pip
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
CMD ["python3", "createrushdatabase.py"]
|
||||
|
||||
FROM node:23
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
# build the frontend
|
||||
COPY ./frontend /frontend
|
||||
WORKDIR /frontend
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
RUN mkdir -p /srv/cprush-stats/frontend
|
||||
RUN cp -rf ./dist/* /srv/cprush-stats/frontend
|
||||
|
||||
# build the backend
|
||||
COPY ./backend /backend
|
||||
WORKDIR /backend
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
RUN mkdir -p /srv/cprush-stats
|
||||
RUN cp -rf ./dist/* /srv/cprush-stats
|
||||
RUN cp package.json /srv/cprush-stats/package.json
|
||||
|
||||
WORKDIR loader
|
||||
|
||||
EXPOSE 3001
|
||||
ENV NODE_ENV=production
|
||||
ENV MEMCACHE_ADDR="localhost:11211"
|
||||
|
||||
COPY --from=loader /srv/cprush-stats/testdb.db /srv/cprush-stats/testdb.db
|
||||
|
||||
WORKDIR /srv/cprush-stats
|
||||
RUN npm install
|
||||
CMD ["sh", "-c", "node app.js -m $MEMCACHE_ADDR"]
|
||||
33
app/backend/.eslintrc.js
Normal file
33
app/backend/.eslintrc.js
Normal file
@@ -0,0 +1,33 @@
|
||||
module.exports = {
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"files": [
|
||||
".eslintrc.{js,cjs}"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
}
|
||||
}
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
}
|
||||
}
|
||||
8
app/backend/.prettierrc.json
Normal file
8
app/backend/.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
6766
app/backend/package-lock.json
generated
Normal file
6766
app/backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
app/backend/package.json
Normal file
43
app/backend/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/",
|
||||
"dev": "npx ts-node-dev src/app.ts",
|
||||
"build": "webpack && cp package.json ./dist/package.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/sequelize": "^4.28.20",
|
||||
"@types/sqlite3": "^3.1.11",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||
"@typescript-eslint/parser": "^7.1.1",
|
||||
"crossenv": "^0.0.2-security",
|
||||
"eslint": "^8.57.0",
|
||||
"node-polyfill-webpack-plugin": "^4.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.4.5",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"asty-astq": "^1.14.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"express": "^4.18.3",
|
||||
"memcached": "^2.2.2",
|
||||
"net": "^1.0.2",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.37.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
"tokenizr": "^1.7.0",
|
||||
"webpack-node-externals": "^3.0.0"
|
||||
}
|
||||
}
|
||||
40
app/backend/src/app.ts
Normal file
40
app/backend/src/app.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import express from 'express'
|
||||
import { json } from 'body-parser'
|
||||
import { Sequelize, Op, QueryTypes } from 'sequelize'
|
||||
import { database, Character, Game, Pick, App } from './db'
|
||||
import { OrderByParser, FilterParser } from './tokenizer'
|
||||
import { addGameApis } from './gameservice'
|
||||
import { addCharacterApis } from './characterservice'
|
||||
import { addRushStatsApis } from './rushstatsservice'
|
||||
import { addDmApis } from './dmservice'
|
||||
import { Memcache } from './memcache'
|
||||
|
||||
var Memcached = require('memcached')
|
||||
|
||||
const app = express()
|
||||
const jsonParser = json()
|
||||
const port = process.argv[process.argv.indexOf('-p')]
|
||||
? process.argv[process.argv.indexOf('-p') + 1]
|
||||
: 3001
|
||||
const memcache_addr = process.argv[process.argv.indexOf('-m')]
|
||||
? process.argv[process.argv.indexOf('-m') + 1]
|
||||
: 'localhost:11211'
|
||||
// const memcache = new memcached('localhost:11211', {})
|
||||
const memcachep = new Memcache(memcache_addr)
|
||||
|
||||
addGameApis(app, jsonParser, memcachep)
|
||||
addCharacterApis(app, jsonParser, memcachep)
|
||||
addRushStatsApis(app, jsonParser, memcachep)
|
||||
addDmApis(app, jsonParser, memcachep)
|
||||
|
||||
app.use(express.static(__dirname + '/frontend'))
|
||||
app.use('/', (req, res) => {
|
||||
res.sendFile(__dirname + '/frontend/index.html')
|
||||
})
|
||||
|
||||
app.listen(port, async () => {
|
||||
await database.authenticate()
|
||||
console.log(`Using memcache at ${memcache_addr}`)
|
||||
console.log(`Express is listening at http://localhost:${port}`)
|
||||
return
|
||||
})
|
||||
97
app/backend/src/characterservice.ts
Normal file
97
app/backend/src/characterservice.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { database, Character, Game, Pick, App } from './db'
|
||||
import { OrderByParser, FilterParser, ParsingError } from './tokenizer'
|
||||
|
||||
export function addCharacterApis(app, jsonParser, memcace) {
|
||||
app.get('/api/character/:characterId', async (req, res) => {
|
||||
try {
|
||||
const character = await Character.findOne({
|
||||
attributes: ['id', 'characterName', 'playerName', 'role', 'creationDate', 'status'],
|
||||
where: { id: req.params['characterId'] }
|
||||
})
|
||||
if (character) {
|
||||
res.send(character)
|
||||
} else {
|
||||
res.status(404).send('Cannot find character.')
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e)
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/character', jsonParser, async (req, res) => {
|
||||
try {
|
||||
const fp = new FilterParser()
|
||||
const obp = new OrderByParser()
|
||||
|
||||
const page = req.body.page
|
||||
const orderBy = req.body.orderBy ? obp.parse(req.body.orderBy) : ['id']
|
||||
const count = req.body.count || 10
|
||||
const filter = req.body.filter ? fp.parse(req.body.filter) : {}
|
||||
|
||||
const characterData = await Character.findAll({
|
||||
offset: page * count,
|
||||
limit: count,
|
||||
order: orderBy,
|
||||
where: filter
|
||||
})
|
||||
const totalCount = await Character.count({
|
||||
where: filter
|
||||
})
|
||||
const pageCount = Math.ceil(totalCount / count)
|
||||
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.send({ characterData, pageCount, totalCount })
|
||||
} catch (e) {
|
||||
if (e instanceof ParsingError) {
|
||||
res.status(400).send('Could not parse filter.')
|
||||
} else {
|
||||
res.status(500).send(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/character/:characterId/apps', async (req, res) => {
|
||||
try {
|
||||
const apps = await App.findAll({ where: { characterId: req.params['characterId'] } })
|
||||
if (apps) {
|
||||
res.send(apps)
|
||||
} else {
|
||||
res.status(400).send('Could not find apps.')
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e)
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/character/:characterId/picks', async (req, res) => {
|
||||
try {
|
||||
const picks = await Pick.findAll({ where: { characterId: req.params['characterId'] } })
|
||||
if (picks) {
|
||||
res.send(picks)
|
||||
} else {
|
||||
res.status(400).send('Could not find picks.')
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e)
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/character/:characterId/gameHistory', async (req, res) => {
|
||||
try {
|
||||
const gameHistory = await Character.findOne({
|
||||
include: [
|
||||
{ model: Game, as: 'characterPickedForGame' },
|
||||
{ model: Game, as: 'characterAppliedForGame' }
|
||||
],
|
||||
where: { id: req.params['characterId'] }
|
||||
})
|
||||
if (gameHistory) {
|
||||
res.send(gameHistory)
|
||||
} else {
|
||||
res.status(404).send('Could nor find character.')
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
102
app/backend/src/db.ts
Normal file
102
app/backend/src/db.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Sequelize, Model, DataTypes } from 'sequelize'
|
||||
|
||||
export type RoleName =
|
||||
| 'Fixer'
|
||||
| 'Tech'
|
||||
| 'Medtech'
|
||||
| 'Media'
|
||||
| 'Netrunner'
|
||||
| 'Solo'
|
||||
| 'Nomad'
|
||||
| 'Exec'
|
||||
| 'Lawman'
|
||||
| 'Rocker'
|
||||
|
||||
export const roleNames: RoleName[] = [
|
||||
'Fixer',
|
||||
'Tech',
|
||||
'Medtech',
|
||||
'Media',
|
||||
'Netrunner',
|
||||
'Solo',
|
||||
'Nomad',
|
||||
'Exec',
|
||||
'Lawman',
|
||||
'Rocker'
|
||||
]
|
||||
|
||||
const databasePath = './testdb.db'
|
||||
|
||||
export const database = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: databasePath
|
||||
})
|
||||
|
||||
export const Character = database.define(
|
||||
'Characters',
|
||||
{
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true },
|
||||
characterName: { type: DataTypes.TEXT },
|
||||
playerName: { type: DataTypes.TEXT },
|
||||
role: { type: DataTypes.TEXT },
|
||||
creationDate: { type: DataTypes.INTEGER },
|
||||
status: { type: DataTypes.TEXT }
|
||||
},
|
||||
{ timestamps: false }
|
||||
)
|
||||
|
||||
export const Game = database.define(
|
||||
'Games',
|
||||
{
|
||||
id: { type: DataTypes.INTEGER, primaryKey: true },
|
||||
title: { type: DataTypes.TEXT },
|
||||
status: { type: DataTypes.TEXT },
|
||||
fix: { type: DataTypes.BOOLEAN },
|
||||
postdate: { type: DataTypes.INTEGER },
|
||||
gamemaster: { type: DataTypes.TEXT },
|
||||
payoutEB: { type: DataTypes.INTEGER },
|
||||
payoutIP: { type: DataTypes.INTEGER },
|
||||
payoutLoot: { type: DataTypes.INTEGER }
|
||||
},
|
||||
{ timestamps: false }
|
||||
)
|
||||
|
||||
export const Pick = database.define(
|
||||
'Picks',
|
||||
{
|
||||
gameId: { type: DataTypes.INTEGER, primaryKey: true },
|
||||
gameTitle: { type: DataTypes.TEXT },
|
||||
characterId: { type: DataTypes.INTEGER, primaryKey: true },
|
||||
characterName: { type: DataTypes.TEXT }
|
||||
},
|
||||
{ timestamps: false }
|
||||
)
|
||||
|
||||
export const App = database.define(
|
||||
'Apps',
|
||||
{
|
||||
gameId: { type: DataTypes.INTEGER, primaryKey: true },
|
||||
gameTitle: { type: DataTypes.TEXT },
|
||||
characterId: { type: DataTypes.INTEGER, primaryKey: true },
|
||||
characterName: { type: DataTypes.TEXT }
|
||||
},
|
||||
{ timestamps: false }
|
||||
)
|
||||
|
||||
// Bind characters to applications and picks
|
||||
Character.hasMany(App, { foreignKey: 'characterId', as: 'appliedCharacter' })
|
||||
App.belongsTo(Character, { foreignKey: 'characterId', as: 'appliedCharacter' })
|
||||
Character.hasMany(Pick, { foreignKey: 'characterId', as: 'pickedCharacter' })
|
||||
Pick.belongsTo(Character, { foreignKey: 'characterId', as: 'pickedCharacter' })
|
||||
|
||||
// Bind games to applications and picks
|
||||
Game.hasMany(App, { foreignKey: 'gameId', as: 'gameApplications' })
|
||||
App.belongsTo(Game, { foreignKey: 'gameId', as: 'gameApplications' })
|
||||
Game.hasMany(Pick, { foreignKey: 'gameId', as: 'gamePicks' })
|
||||
Pick.belongsTo(Game, { foreignKey: 'gameId', as: 'gamePicks' })
|
||||
|
||||
// Bind picked characters to games.
|
||||
Game.belongsToMany(Character, { through: 'Apps', as: 'characterAppliedForGame' })
|
||||
Character.belongsToMany(Game, { through: 'Apps', as: 'characterAppliedForGame' })
|
||||
Game.belongsToMany(Character, { through: 'Picks', as: 'characterPickedForGame' })
|
||||
Character.belongsToMany(Game, { through: 'Picks', as: 'characterPickedForGame' })
|
||||
58
app/backend/src/dmservice.ts
Normal file
58
app/backend/src/dmservice.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { database, Character, Game, Pick, App } from './db'
|
||||
import { OrderByParser, FilterParser, ParsingError } from './tokenizer'
|
||||
import { fn, col } from 'sequelize'
|
||||
|
||||
export function addDmApis(app, jsonParser, memcache) {
|
||||
app.get('/api/dm/:dmName', async (req, res) => {
|
||||
const dmName = req.params.dmName
|
||||
|
||||
let games = await Game.findAll({
|
||||
where: { gamemaster: dmName }
|
||||
})
|
||||
|
||||
res.send(games)
|
||||
})
|
||||
|
||||
app.post('/api/dm', jsonParser, async (req, res) => {
|
||||
try {
|
||||
const fp = new FilterParser()
|
||||
const obp = new OrderByParser()
|
||||
|
||||
const page = req.body.page || 0
|
||||
const orderBy = req.body.orderBy ? obp.parse(req.body.orderBy) : ['id']
|
||||
const count = req.body.count || 10
|
||||
const filter = req.body.filter ? fp.parse(req.body.filter) : {}
|
||||
|
||||
const dms = await Game.findAll({
|
||||
offset: page * count,
|
||||
limit: count,
|
||||
attributes: ['gamemaster', [fn('count', col('id')), 'gamecount']],
|
||||
group: ['gamemaster'],
|
||||
order: orderBy,
|
||||
where: filter
|
||||
})
|
||||
|
||||
const totalCount = (
|
||||
await Game.count({
|
||||
group: ['gamemaster'],
|
||||
where: filter
|
||||
})
|
||||
).length
|
||||
|
||||
const pageCount = Math.ceil(totalCount / count)
|
||||
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.send({
|
||||
dms,
|
||||
pageCount,
|
||||
totalCount
|
||||
})
|
||||
} catch (e) {
|
||||
if (e instanceof ParsingError) {
|
||||
res.status(400).send('Could not parse filter.')
|
||||
} else {
|
||||
res.status(500).send(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
82
app/backend/src/gameservice.ts
Normal file
82
app/backend/src/gameservice.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { database, Character, Game, Pick, App } from './db'
|
||||
import { OrderByParser, FilterParser, ParsingError } from './tokenizer'
|
||||
|
||||
export function addGameApis(app, jsonParser, memcache) {
|
||||
app.get('/api/game/:gameId', async (req, res) => {
|
||||
try {
|
||||
const game = await Game.findOne({ where: { id: req.params['gameId'] } })
|
||||
if (!game) {
|
||||
res.status(404).send('Game Not Found')
|
||||
} else {
|
||||
res.send(game)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.status(500).send(e)
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/game', jsonParser, async (req, res) => {
|
||||
try {
|
||||
const fp = new FilterParser()
|
||||
const obp = new OrderByParser()
|
||||
|
||||
const page = req.body.page || 0
|
||||
const orderBy = req.body.orderBy ? obp.parse(req.body.orderBy) : ['id']
|
||||
const count = req.body.count || 10
|
||||
const filter = req.body.filter ? fp.parse(req.body.filter) : {}
|
||||
|
||||
const gameData = await Game.findAll({
|
||||
offset: page * count,
|
||||
limit: count,
|
||||
order: orderBy,
|
||||
where: filter
|
||||
})
|
||||
const totalCount = await Game.count({
|
||||
where: filter
|
||||
})
|
||||
const pageCount = Math.ceil(totalCount / count)
|
||||
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.send({ gameData, pageCount, totalCount })
|
||||
} catch (e) {
|
||||
if (e instanceof ParsingError) {
|
||||
res.status(400).send('Could not parse filter.')
|
||||
} else {
|
||||
res.status(500).send(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/game/:gameId/apps', async (req, res) => {
|
||||
try {
|
||||
const apps = await App.findAll({
|
||||
include: { model: Character, as: 'appliedCharacter' },
|
||||
where: { gameId: req.params['gameId'] }
|
||||
})
|
||||
if (!apps) {
|
||||
res.status(404).send('Apps not found.')
|
||||
} else {
|
||||
res.send(apps)
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e)
|
||||
}
|
||||
})
|
||||
|
||||
app.post('/api/game/:gameId/picks', async (req, res) => {
|
||||
try {
|
||||
const picks = await Pick.findAll({
|
||||
include: { model: Character, as: 'pickedCharacter' },
|
||||
where: { gameId: req.params['gameId'] }
|
||||
})
|
||||
if (!picks) {
|
||||
res.status(404).send('Picks not found')
|
||||
} else {
|
||||
res.send(picks)
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
29
app/backend/src/memcache.ts
Normal file
29
app/backend/src/memcache.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { promisify } from 'util'
|
||||
|
||||
var Memcached = require('memcached')
|
||||
|
||||
export class Memcache {
|
||||
readonly memcached
|
||||
|
||||
readonly getCallback
|
||||
readonly setCallback
|
||||
readonly replaceCallback
|
||||
|
||||
constructor(url: string) {
|
||||
this.memcached = new Memcached(url, {
|
||||
timeout: 30
|
||||
})
|
||||
|
||||
this.getCallback = promisify(this.memcached.get).bind(this.memcached)
|
||||
this.setCallback = promisify(this.memcached.set).bind(this.memcached)
|
||||
this.replaceCallback = promisify(this.memcached.replace).bind(this.memcached)
|
||||
}
|
||||
|
||||
async get(key: string) {
|
||||
return this.getCallback(key)
|
||||
}
|
||||
|
||||
async set(key: string, value) {
|
||||
return this.setCallback(key, value, 200)
|
||||
}
|
||||
}
|
||||
134
app/backend/src/rushstatsservice.ts
Normal file
134
app/backend/src/rushstatsservice.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { database, Character, Game, Pick, App, roleNames } from './db'
|
||||
import { Op, QueryTypes } from 'sequelize'
|
||||
|
||||
const serverGameStatsQuery =
|
||||
'SELECT ' +
|
||||
'COUNT(*) as TotalGames, ' +
|
||||
'SUM(CASE WHEN status = "Complete" THEN 1 ELSE 0 END) as Complete, ' +
|
||||
'SUM(CASE WHEN status = "Postponed" THEN 1 ELSE 0 END) as Postponed, ' +
|
||||
'SUM(CASE WHEN status = "Pending" THEN 1 ELSE 0 END) as Pending, ' +
|
||||
'SUM(CASE WHEN event = "TRUE" THEN 1 ELSE 0 END) as Events, ' +
|
||||
'SUM(CASE WHEN fix = "TRUE" THEN 1 ELSE 0 END) as Fixes, ' +
|
||||
'SUM(payoutEB) as TotalEB, ' +
|
||||
'SUM(payoutIP) as TotalIP, ' +
|
||||
'SUM(payoutEB) / SUM(CASE WHEN status = "Complete" THEN 1 ELSE 0 END) as AverageEB, ' +
|
||||
'SUM(payoutIP) / SUM(CASE WHEN status = "Complete" THEN 1 ELSE 0 END) as AverageIP ' +
|
||||
'FROM Games WHERE postdate BETWEEN :startSeconds AND :endSeconds'
|
||||
|
||||
const startingYear = 2023
|
||||
|
||||
function getMonthNumber(monthId: number): number {
|
||||
return monthId % 12
|
||||
}
|
||||
|
||||
function getYearNumber(monthId: number): number {
|
||||
return Math.floor(monthId / 12 + startingYear)
|
||||
}
|
||||
|
||||
function monthIdToStartSeconds(monthId: number): number {
|
||||
if (monthId != -1) {
|
||||
const yearNumber = getYearNumber(monthId)
|
||||
const monthNumber = getMonthNumber(monthId)
|
||||
return new Date(yearNumber, monthNumber, 1).getTime() / 1000
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function monthIdToEndSeconds(monthId: number): number {
|
||||
if (monthId != -1) {
|
||||
const yearNumber = getYearNumber(monthId)
|
||||
const monthNumber = getMonthNumber(monthId)
|
||||
return new Date(yearNumber, monthNumber + 1, -1).getTime() / 1000
|
||||
}
|
||||
return Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
|
||||
export function addRushStatsApis(app, jsonParser, memcache) {
|
||||
app.post('/api/serverstats/gamestats', jsonParser, async (req, res) => {
|
||||
const monthId = req.body.monthId
|
||||
let startSeconds = monthIdToStartSeconds(monthId)
|
||||
let endSeconds = monthIdToEndSeconds(monthId)
|
||||
|
||||
let cachedResponse = await memcache.get('gamestats' + req.body.monthId)
|
||||
|
||||
if (cachedResponse) {
|
||||
console.info('cache hit - ' + 'gamestats' + req.body.monthId)
|
||||
res.send(cachedResponse)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('cache miss')
|
||||
const serverGameStats = await database.query(serverGameStatsQuery, {
|
||||
type: QueryTypes.SELECT,
|
||||
replacements: {
|
||||
startSeconds,
|
||||
endSeconds
|
||||
}
|
||||
})
|
||||
|
||||
await memcache.set('gamestats' + req.body.monthId, serverGameStats[0])
|
||||
res.send(serverGameStats[0])
|
||||
})
|
||||
|
||||
app.post('/api/serverstats/rolestats', jsonParser, async (req, res) => {
|
||||
const monthId = req.body.monthId
|
||||
let startSeconds = monthIdToStartSeconds(monthId)
|
||||
let endSeconds = monthIdToEndSeconds(monthId)
|
||||
|
||||
let cachedResponse = await memcache.get('rolestats' + req.body.monthId)
|
||||
|
||||
if (cachedResponse) {
|
||||
console.info('cache hit - ' + 'rolestats' + req.body.monthId)
|
||||
res.send(cachedResponse)
|
||||
return
|
||||
}
|
||||
const games = await Game.findAll({
|
||||
include: [
|
||||
{ model: Character, as: 'characterPickedForGame' },
|
||||
{ model: Character, as: 'characterAppliedForGame' }
|
||||
],
|
||||
where: { postdate: { [Op.between]: [startSeconds, endSeconds] } }
|
||||
})
|
||||
|
||||
// count active roles
|
||||
let activeCharacters = new Map()
|
||||
let pickedCharacterCount = new Map()
|
||||
let appedCharacterCount = new Map()
|
||||
|
||||
games.forEach((game, gameNum) => {
|
||||
const picks = game.dataValues.characterPickedForGame
|
||||
const appls = game.dataValues.characterAppliedForGame
|
||||
|
||||
picks.forEach((character, characterNum) => {
|
||||
const role = character.dataValues.role
|
||||
// Count role application
|
||||
pickedCharacterCount.set(role, (pickedCharacterCount.get(role) || 0) + 1)
|
||||
})
|
||||
|
||||
appls.forEach((character, characterNum) => {
|
||||
const role = character.dataValues.role
|
||||
const charId = character.dataValues.id
|
||||
// Add apllied characters to active list
|
||||
if (!activeCharacters.has(role)) {
|
||||
activeCharacters.set(role, new Set())
|
||||
}
|
||||
activeCharacters.get(role).add(charId)
|
||||
// Count role application
|
||||
appedCharacterCount.set(role, (appedCharacterCount.get(role) || 0) + 1)
|
||||
})
|
||||
})
|
||||
|
||||
const result = {}
|
||||
|
||||
roleNames.forEach((roleName) => {
|
||||
result[roleName] = {
|
||||
apps: appedCharacterCount.get(roleName) || 0,
|
||||
picks: pickedCharacterCount.get(roleName) || 0,
|
||||
active: activeCharacters.has(roleName) ? activeCharacters.get(roleName).size : 0
|
||||
}
|
||||
})
|
||||
|
||||
await memcache.set('rolestats' + req.body.monthId, result)
|
||||
res.send(result)
|
||||
})
|
||||
}
|
||||
177
app/backend/src/tokenizer.ts
Normal file
177
app/backend/src/tokenizer.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import Tokenizr from 'tokenizr'
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
export { ParsingError } from 'tokenizr'
|
||||
|
||||
export class OrderByParser {
|
||||
lexer = new Tokenizr()
|
||||
|
||||
constructor() {
|
||||
this.lexer.rule(/,/, (ctx, m) => {
|
||||
ctx.accept('spacer')
|
||||
})
|
||||
|
||||
this.lexer.rule(/ASC|DESC/, (ctx, m) => {
|
||||
ctx.accept('direction', m[0])
|
||||
})
|
||||
|
||||
this.lexer.rule(/[a-zA-Z]+/, (ctx, m) => {
|
||||
ctx.accept('column', m[0])
|
||||
})
|
||||
|
||||
this.lexer.rule(/\s/, (ctx, m) => {
|
||||
ctx.ignore()
|
||||
})
|
||||
}
|
||||
|
||||
parse(orderBy: string) {
|
||||
const output = []
|
||||
let holding = []
|
||||
this.lexer
|
||||
.input(orderBy)
|
||||
.tokens()
|
||||
.forEach((token) => {
|
||||
switch (token.type) {
|
||||
case 'spacer':
|
||||
output.push(holding)
|
||||
holding = []
|
||||
break
|
||||
case 'column':
|
||||
case 'direction':
|
||||
holding.push(token.value)
|
||||
break
|
||||
}
|
||||
})
|
||||
if (holding) {
|
||||
output.push(holding)
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
const openGroupRegex = /\(/
|
||||
const closeGroupRegex = /\)/
|
||||
const conjunctinoRegex = /AND|OR/
|
||||
const equalityRegex = /([a-zA-Z]+)\s?(=|!=|<|>|<=|>=|:)\s?([\d]+)/
|
||||
const stringEqualityRegex = /([a-zA-Z]+)\s?(=|!=|<|>|<=|>=|:)\s?\"([a-zA-Z'"\d\s]*)\"/
|
||||
const spacerRegex = /\s/
|
||||
|
||||
const opperatorMap = {
|
||||
'=': Op.eq,
|
||||
'!=': Op.ne,
|
||||
'<=': Op.lte,
|
||||
'>=': Op.gte,
|
||||
'>': Op.gt,
|
||||
'<': Op.lt,
|
||||
':': Op.like,
|
||||
AND: Op.and,
|
||||
OR: Op.or
|
||||
}
|
||||
|
||||
export class FilterParser {
|
||||
lexer: Tokenizr
|
||||
|
||||
constructor() {
|
||||
this.lexer = new Tokenizr()
|
||||
|
||||
this.lexer.rule(openGroupRegex, (ctx, m) => {
|
||||
ctx.accept('opengroup')
|
||||
})
|
||||
|
||||
this.lexer.rule(closeGroupRegex, (ctx, m) => {
|
||||
ctx.accept('closegroup')
|
||||
})
|
||||
|
||||
this.lexer.rule(conjunctinoRegex, (ctx, m) => {
|
||||
ctx.accept('conjunction', m[0])
|
||||
})
|
||||
|
||||
this.lexer.rule(stringEqualityRegex, (ctx, m) => {
|
||||
ctx.accept('column', m[1])
|
||||
ctx.accept('opperator', m[2])
|
||||
ctx.accept('value', m[3])
|
||||
})
|
||||
|
||||
this.lexer.rule(equalityRegex, (ctx, m) => {
|
||||
ctx.accept('column', m[1])
|
||||
ctx.accept('opperator', m[2])
|
||||
ctx.accept('value', m[3])
|
||||
})
|
||||
|
||||
this.lexer.rule(spacerRegex, (ctx, m) => {
|
||||
ctx.ignore()
|
||||
})
|
||||
}
|
||||
|
||||
parse(filter: string) {
|
||||
this.lexer.input(filter)
|
||||
let block = this.parseBlock('AND')
|
||||
this.lexer.consume('EOF')
|
||||
this.lexer.reset()
|
||||
return block
|
||||
}
|
||||
|
||||
private parseBlock(conjunciton: string) {
|
||||
let items = []
|
||||
let activeCon = conjunciton
|
||||
|
||||
for (;;) {
|
||||
let nextItem = this.lexer.alternatives(
|
||||
() => this.parseEquality(),
|
||||
() => this.parseGroup(),
|
||||
() => {
|
||||
let conToken = this.lexer.consume('conjunction')
|
||||
|
||||
if (items.length === 1) {
|
||||
activeCon = conToken.value
|
||||
}
|
||||
|
||||
if (conToken.value === activeCon) {
|
||||
return this.parseEquality()
|
||||
} else {
|
||||
return this.parseBlock(conToken.value)
|
||||
}
|
||||
},
|
||||
() => this.parseEmpty()
|
||||
)
|
||||
|
||||
if (nextItem === undefined) {
|
||||
break
|
||||
}
|
||||
|
||||
items.push(nextItem)
|
||||
}
|
||||
|
||||
return { [opperatorMap[activeCon]]: items }
|
||||
}
|
||||
|
||||
private parseEquality() {
|
||||
let columnToken = this.lexer.consume('column')
|
||||
let opperatorToken = this.lexer.consume('opperator')
|
||||
let valueToken = this.lexer.consume('value')
|
||||
|
||||
if (opperatorToken.value === ':') {
|
||||
return {
|
||||
[columnToken.value]: {
|
||||
[opperatorMap[opperatorToken.value]]: `%${valueToken.value.toString()}%`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
[columnToken.value]: { [opperatorMap[opperatorToken.value]]: valueToken.value.toString() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseGroup() {
|
||||
this.lexer.consume('opengroup')
|
||||
let block = this.parseBlock('AND')
|
||||
this.lexer.consume('closegroup')
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
private parseEmpty() {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
11
app/backend/tsconfig.json
Normal file
11
app/backend/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es6",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist"
|
||||
},
|
||||
"lib": ["es2015"]
|
||||
}
|
||||
30
app/backend/webpack.config.js
Normal file
30
app/backend/webpack.config.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const path = require('path');
|
||||
const NodePolyfillPlugin = require("node-polyfill-webpack-plugin")
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: './src/app.ts',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
}, node: {
|
||||
__dirname: false
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js'],
|
||||
},
|
||||
output: {
|
||||
filename: 'app.js',
|
||||
path: path.resolve(__dirname, './dist'),
|
||||
},
|
||||
plugins: [
|
||||
new NodePolyfillPlugin(),
|
||||
],
|
||||
externals: [nodeExternals()],
|
||||
};
|
||||
4
app/frontend/.browserslistrc
Normal file
4
app/frontend/.browserslistrc
Normal file
@@ -0,0 +1,4 @@
|
||||
> 1%
|
||||
last 2 versions
|
||||
not dead
|
||||
not ie 11
|
||||
5
app/frontend/.editorconfig
Normal file
5
app/frontend/.editorconfig
Normal file
@@ -0,0 +1,5 @@
|
||||
[*.{js,jsx,ts,tsx,vue}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
1
app/frontend/.env.dev
Normal file
1
app/frontend/.env.dev
Normal file
@@ -0,0 +1 @@
|
||||
VITE_API_TARGET=http://localhost:3001
|
||||
0
app/frontend/.env.prod
Normal file
0
app/frontend/.env.prod
Normal file
72
app/frontend/.eslintrc-auto-import.json
Normal file
72
app/frontend/.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"computed": true,
|
||||
"createApp": true,
|
||||
"customRef": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"effectScope": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"inject": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeRouteLeave": true,
|
||||
"onBeforeRouteUpdate": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"provide": true,
|
||||
"reactive": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"resolveComponent": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"toRaw": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"unref": true,
|
||||
"useAttrs": true,
|
||||
"useCssModule": true,
|
||||
"useCssVars": true,
|
||||
"useLink": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useSlots": true,
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
}
|
||||
}
|
||||
20
app/frontend/.eslintrc.js
Normal file
20
app/frontend/.eslintrc.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* .eslint.js
|
||||
*
|
||||
* ESLint configuration file.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/vue3-essential',
|
||||
'eslint:recommended',
|
||||
'@vue/eslint-config-typescript',
|
||||
],
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
},
|
||||
}
|
||||
8
app/frontend/.prettierrc.json
Normal file
8
app/frontend/.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
81
app/frontend/README.md
Normal file
81
app/frontend/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Vuetify (Default)
|
||||
|
||||
This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
|
||||
|
||||
## ❗️ Important Links
|
||||
|
||||
- 📄 [Docs](https://vuetifyjs.com/)
|
||||
- 🚨 [Issues](https://issues.vuetifyjs.com/)
|
||||
- 🏬 [Store](https://store.vuetifyjs.com/)
|
||||
- 🎮 [Playground](https://play.vuetifyjs.com/)
|
||||
- 💬 [Discord](https://community.vuetifyjs.com)
|
||||
|
||||
## 💿 Install
|
||||
|
||||
Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
|
||||
|
||||
| Package Manager | Command |
|
||||
|---------------------------------------------------------------|----------------|
|
||||
| [yarn](https://yarnpkg.com/getting-started) | `yarn install` |
|
||||
| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` |
|
||||
| [pnpm](https://pnpm.io/installation) | `pnpm install` |
|
||||
| [bun](https://bun.sh/#getting-started) | `bun install` |
|
||||
|
||||
After completing the installation, your environment is ready for Vuetify development.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
|
||||
- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
|
||||
- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts)
|
||||
- 💻 **Enhanced Development Experience**: Benefit from TypeScript's static type checking and the ESLint plugin suite for Vue, ensuring code quality and consistency. [TypeScript](https://www.typescriptlang.org/) | [ESLint Plugin Vue](https://eslint.vuejs.org/)
|
||||
- ⚡ **Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
|
||||
- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
|
||||
- 🛠️ **Strongly-Typed Vue**: Use vue-tsc for type-checking your Vue components, and enjoy a robust development experience. [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc)
|
||||
|
||||
These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
|
||||
|
||||
## 💡 Usage
|
||||
|
||||
This section covers how to start the development server and build your project for production.
|
||||
|
||||
### Starting the Development Server
|
||||
|
||||
To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000):
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||
|
||||
> NODE_OPTIONS='--no-warnings' is added to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
|
||||
|
||||
### Building for Production
|
||||
|
||||
To build your project for production, use:
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
(Repeat for npm, pnpm, and bun with respective commands.)
|
||||
|
||||
Once the build process is completed, your application will be ready for deployment in a production environment.
|
||||
|
||||
## 💪 Support Vuetify Development
|
||||
|
||||
This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
|
||||
|
||||
- [Requesting Enterprise Support](https://support.vuetifyjs.com/)
|
||||
- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
|
||||
- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
|
||||
- [Supporting the team on Open Collective](https://opencollective.com/vuetify)
|
||||
- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
|
||||
- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
|
||||
- [Making a one-time donation with Paypal](https://paypal.me/vuetify)
|
||||
|
||||
## 📑 License
|
||||
[MIT](http://opensource.org/licenses/MIT)
|
||||
|
||||
Copyright (c) 2016-present Vuetify, LLC
|
||||
197
app/frontend/auto-imports.d.ts
vendored
Normal file
197
app/frontend/auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module 'vue' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
}
|
||||
}
|
||||
declare module '@vue/runtime-core' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router')['useLink']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
}
|
||||
}
|
||||
13
app/frontend/components.d.ts
vendored
Normal file
13
app/frontend/components.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
16
app/frontend/index.html
Normal file
16
app/frontend/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Rush Stats</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
6131
app/frontend/package-lock.json
generated
Normal file
6131
app/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
app/frontend/package.json
Normal file
49
app/frontend/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "rushstats",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_OPTIONS='--no-warnings' vite --mode dev",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.0.96",
|
||||
"axios": "^1.6.8",
|
||||
"chart.js": "^4.4.3",
|
||||
"core-js": "^3.34.0",
|
||||
"roboto-fontface": "*",
|
||||
"sqlite3": "^5.1.7",
|
||||
"vue": "^3.3.0",
|
||||
"vuetify": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.23.0",
|
||||
"@types/node": "^20.10.0",
|
||||
"@vitejs/plugin-vue": "^4.5.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-n": "^16.4.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-vue": "^9.19.0",
|
||||
"pinia": "^2.1.0",
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.69.0",
|
||||
"typescript": "^5.3.0",
|
||||
"typescript-eslint": "^7.0.2",
|
||||
"unplugin-auto-import": "^0.17.3",
|
||||
"unplugin-fonts": "^1.1.0",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"unplugin-vue-router": "^0.7.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-vue-layouts": "^0.10.0",
|
||||
"vite-plugin-vuetify": "^2.0.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue-tsc": "^1.8.0"
|
||||
}
|
||||
}
|
||||
BIN
app/frontend/public/favicon.ico
Normal file
BIN
app/frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
15
app/frontend/src/App.vue
Normal file
15
app/frontend/src/App.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<v-app-bar>
|
||||
<v-btn to="/serverstats">Server Stats</v-btn>
|
||||
<v-btn to="/games">Games</v-btn>
|
||||
<v-btn to="/characters">Characters</v-btn>
|
||||
<!-- <v-btn to="/gamemasters">Gamemasters</v-btn> -->
|
||||
</v-app-bar>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
BIN
app/frontend/src/assets/logo.png
Normal file
BIN
app/frontend/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
6
app/frontend/src/assets/logo.svg
Normal file
6
app/frontend/src/assets/logo.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
|
||||
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
|
||||
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
|
||||
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 526 B |
20
app/frontend/src/main.ts
Normal file
20
app/frontend/src/main.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* main.ts
|
||||
*
|
||||
* Bootstraps Vuetify and other plugins then mounts the App`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import { registerPlugins } from '@/plugins'
|
||||
|
||||
// Components
|
||||
import App from './App.vue'
|
||||
|
||||
// Composables
|
||||
import { createApp } from 'vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
registerPlugins(app)
|
||||
|
||||
app.mount('#app')
|
||||
16
app/frontend/src/plugins/index.ts
Normal file
16
app/frontend/src/plugins/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* plugins/index.ts
|
||||
*
|
||||
* Automatically included in `./src/main.ts`
|
||||
*/
|
||||
|
||||
// Plugins
|
||||
import vuetify from './vuetify'
|
||||
import router from '../router'
|
||||
|
||||
// Types
|
||||
import type { App } from 'vue'
|
||||
|
||||
export function registerPlugins(app: App) {
|
||||
app.use(vuetify).use(router)
|
||||
}
|
||||
17
app/frontend/src/plugins/vuetify.ts
Normal file
17
app/frontend/src/plugins/vuetify.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* plugins/vuetify.ts
|
||||
*
|
||||
* Framework documentation: https://vuetifyjs.com`
|
||||
*/
|
||||
|
||||
// Styles
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles'
|
||||
|
||||
// Composables
|
||||
import { createVuetify } from 'vuetify'
|
||||
|
||||
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||
export default createVuetify({
|
||||
theme: { defaultTheme: 'dark' }
|
||||
})
|
||||
60
app/frontend/src/router/index.ts
Normal file
60
app/frontend/src/router/index.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import GameList from '../vues/GameList.vue'
|
||||
import GameDetails from '../vues/GameDetails.vue'
|
||||
import CharacterList from '../vues/CharacterList.vue'
|
||||
import CharacterDetails from '../vues/CharacterDetails.vue'
|
||||
import ServerStats from '../vues/ServerStats.vue'
|
||||
import DmList from '../vues/DmList.vue'
|
||||
|
||||
const root = {
|
||||
path: '/',
|
||||
redirect: '/serverstats'
|
||||
}
|
||||
|
||||
const gameListRoute = {
|
||||
path: '/games',
|
||||
component: GameList
|
||||
}
|
||||
|
||||
const gameDetailsRoute = {
|
||||
name: 'game',
|
||||
path: '/games/:gameId',
|
||||
component: GameDetails
|
||||
}
|
||||
|
||||
const characterListRoute = {
|
||||
path: '/characters',
|
||||
component: CharacterList
|
||||
}
|
||||
|
||||
const characterDetailsRoute = {
|
||||
name: 'character',
|
||||
path: '/characters/:characterId',
|
||||
component: CharacterDetails
|
||||
}
|
||||
|
||||
const serverStatsRoute = {
|
||||
path: '/serverstats',
|
||||
component: ServerStats
|
||||
}
|
||||
|
||||
const gameMasterListRoute = {
|
||||
path: '/gamemasters',
|
||||
component: DmList
|
||||
}
|
||||
|
||||
const routes = [
|
||||
root,
|
||||
gameListRoute,
|
||||
gameDetailsRoute,
|
||||
characterListRoute,
|
||||
characterDetailsRoute,
|
||||
serverStatsRoute,
|
||||
gameMasterListRoute
|
||||
]
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
})
|
||||
10
app/frontend/src/styles/settings.scss
Normal file
10
app/frontend/src/styles/settings.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* src/styles/settings.scss
|
||||
*
|
||||
* Configures SASS variables and Vuetify overwrites
|
||||
*/
|
||||
|
||||
// https://vuetifyjs.com/features/sass-variables/`
|
||||
// @use 'vuetify/settings' with (
|
||||
// $color-pack: false
|
||||
// );
|
||||
69
app/frontend/src/types.ts
Normal file
69
app/frontend/src/types.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export interface Game {
|
||||
id: number
|
||||
title: string
|
||||
gamemaster: string
|
||||
payoutEB: number
|
||||
payoutIP: number
|
||||
payoutLoot: string
|
||||
status: string
|
||||
postdate: number
|
||||
pickedCharacter?: Character[]
|
||||
appliedCharacter?: Character[]
|
||||
}
|
||||
|
||||
export interface Character {
|
||||
id: number
|
||||
characterName: string
|
||||
playerName: string
|
||||
role: string
|
||||
status: string
|
||||
lastGame: number
|
||||
creationDate: number
|
||||
}
|
||||
|
||||
export interface DmStats {
|
||||
name: string
|
||||
gameCount: string
|
||||
lastGame: number
|
||||
games: Game
|
||||
}
|
||||
|
||||
export interface GameStats {
|
||||
Complete: number
|
||||
Postponed: number
|
||||
Pending: number
|
||||
Fixes: number
|
||||
Events: number
|
||||
AverageIP: number
|
||||
AverageEB: number
|
||||
TotalIP: number
|
||||
TotalEB: number
|
||||
}
|
||||
|
||||
export interface GameCounts {
|
||||
apps: number
|
||||
picks: number
|
||||
active: number
|
||||
}
|
||||
|
||||
export interface GameCharacter {
|
||||
characterId: number
|
||||
characterName: string
|
||||
gameId: number
|
||||
gameTite: string
|
||||
pickedCharacter?: Character
|
||||
appliedCharacter?: Character
|
||||
}
|
||||
|
||||
export interface RoleStats {
|
||||
Fixer: GameCounts
|
||||
Tech: GameCounts
|
||||
Medtech: GameCounts
|
||||
Media: GameCounts
|
||||
Netrunner: GameCounts
|
||||
Solo: GameCounts
|
||||
Nomad: GameCounts
|
||||
Exec: GameCounts
|
||||
Lawman: GameCounts
|
||||
Rocker: GameCounts
|
||||
}
|
||||
7
app/frontend/src/vite-env.d.ts
vendored
Normal file
7
app/frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
127
app/frontend/src/vues/CharacterDetails.vue
Normal file
127
app/frontend/src/vues/CharacterDetails.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<h1>{{ character.characterName }} - {{ character.role }}</h1>
|
||||
<h2>Status: {{ character.status }}</h2>
|
||||
<h2>Player: {{ character.playerName }}</h2>
|
||||
<div>Total Game EB: {{ earnedEB }} --- Total Game IP: {{ earnedIP }}</div>
|
||||
<div>
|
||||
Games Played: {{ gamesPlayedCount }} --- Games Applied:
|
||||
{{ gamesAppliedCount }} -- Pick Rate: {{ pickRate.toFixed(2) }}%
|
||||
</div>
|
||||
<div>Last Game: {{ lastPlayedPostDate }}</div>
|
||||
<div class="d-flex flex-row">
|
||||
<div class="flex-column game-list">
|
||||
<h3 class="game-list-title">
|
||||
{{ (games.played || []).length }} Games Played
|
||||
</h3>
|
||||
<GameTable :gameList="games.played"></GameTable>
|
||||
</div>
|
||||
<div class="flex-column game-list">
|
||||
<h3 class="game-list-title">
|
||||
{{ (games.applied || []).length }} Games Applied
|
||||
</h3>
|
||||
<game-table :gameList="games.applied"></game-table>
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.game-list {
|
||||
margin: 20px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
.game-list-title {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Character, Game } from '../types'
|
||||
import GameTable from './GameTable.vue'
|
||||
import { onMounted, watch, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
|
||||
interface GameList {
|
||||
played: Game[]
|
||||
applied: Game[]
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const characterId = ref(route.params.characterId)
|
||||
const character = ref<Character>({
|
||||
id: 0,
|
||||
characterName: '',
|
||||
playerName: '',
|
||||
role: '',
|
||||
status: '',
|
||||
lastGame: 0,
|
||||
creationDate: 0
|
||||
})
|
||||
const games = ref<GameList>({ played: [], applied: [] })
|
||||
const earnedEB = ref(0)
|
||||
const earnedIP = ref(0)
|
||||
const gamesPlayedCount = ref(0)
|
||||
const gamesAppliedCount = ref(0)
|
||||
const pickRate = ref(0)
|
||||
const lastPlayedPostDate = ref()
|
||||
|
||||
async function loadCharacterDetails() {
|
||||
const characterResponse = await axios.get(
|
||||
`/api/character/${characterId.value}`
|
||||
)
|
||||
const gameDetails = await axios.post(
|
||||
`/api/character/${characterId.value}/gameHistory`
|
||||
)
|
||||
|
||||
character.value = characterResponse.data
|
||||
games.value = {
|
||||
played: gameDetails.data.characterPickedForGame,
|
||||
applied: gameDetails.data.characterAppliedForGame
|
||||
}
|
||||
}
|
||||
|
||||
function calculateDerivedEarnings(gamesPlayedList: Game[]) {
|
||||
let runningEarnedEb = 0
|
||||
let runningEarnedIp = 0
|
||||
for (let game in gamesPlayedList) {
|
||||
runningEarnedEb += gamesPlayedList[game]['payoutEB']
|
||||
runningEarnedIp += gamesPlayedList[game]['payoutIP']
|
||||
}
|
||||
earnedEB.value = runningEarnedEb
|
||||
earnedIP.value = runningEarnedIp
|
||||
}
|
||||
|
||||
function calculateDerivedGameStats(
|
||||
gamesPlayedList: Game[],
|
||||
gamesAppliedList: Game[]
|
||||
) {
|
||||
gamesPlayedCount.value = gamesPlayedList.length
|
||||
gamesAppliedCount.value = gamesAppliedList.length
|
||||
pickRate.value = (gamesPlayedList.length / gamesAppliedList.length) * 100
|
||||
}
|
||||
|
||||
function calculateDerivedLastPlayedPostDate(gamesPlayedList: Game[]) {
|
||||
if (gamesPlayedList.length === 0) {
|
||||
lastPlayedPostDate.value = 'N/A'
|
||||
} else {
|
||||
lastPlayedPostDate.value = new Date(
|
||||
gamesPlayedList[gamesPlayedList.length - 1].postdate * 1000
|
||||
)
|
||||
.toISOString()
|
||||
.split('T')[0]
|
||||
}
|
||||
}
|
||||
|
||||
watch(games, (newValue: GameList, oldValue: GameList) => {
|
||||
calculateDerivedLastPlayedPostDate(newValue.played)
|
||||
calculateDerivedEarnings(newValue.played)
|
||||
calculateDerivedGameStats(newValue.played, newValue.applied)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
loadCharacterDetails()
|
||||
})
|
||||
</script>
|
||||
171
app/frontend/src/vues/CharacterList.vue
Normal file
171
app/frontend/src/vues/CharacterList.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div class="d-flex flex-row">
|
||||
<v-text-field
|
||||
class="filter flex-1-0"
|
||||
placeholder="Filter by Name"
|
||||
v-model="filterValue"
|
||||
></v-text-field>
|
||||
</div>
|
||||
<v-data-table-server
|
||||
:headers="headers"
|
||||
:items="characters"
|
||||
:items-length="count"
|
||||
v-model:items-per-page="resultePerPage"
|
||||
v-model:page="pageValue"
|
||||
v-model:sort-by="sortValue"
|
||||
@update:options="loadData"
|
||||
>
|
||||
<template v-slot:item.id="{ item }">
|
||||
<router-link
|
||||
:to="{ name: 'character', params: { characterId: item.id } }"
|
||||
>{{ item.id }}</router-link
|
||||
>
|
||||
</template>
|
||||
<template v-slot:item.characterName="{ item }">
|
||||
<router-link
|
||||
:to="{ name: 'character', params: { characterId: item.id } }"
|
||||
>{{ item.characterName }}</router-link
|
||||
>
|
||||
</template>
|
||||
<template v-slot:item.creationDate="{ item }">
|
||||
{{ new Date(item.creationDate * 1000).toISOString().split('T')[0] }}
|
||||
</template>
|
||||
<template #bottom></template>
|
||||
</v-data-table-server>
|
||||
<v-pagination
|
||||
v-model:model-value="pageValue"
|
||||
:length="pageCount"
|
||||
></v-pagination>
|
||||
</template>
|
||||
|
||||
<style type="text/css"></style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Character } from '../types'
|
||||
import { onBeforeMount, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter, LocationQueryValue } from 'vue-router'
|
||||
import { VDataTable } from 'vuetify/components'
|
||||
import axios from 'axios'
|
||||
|
||||
type ReadonlyHeaders = VDataTable['$props']['headers']
|
||||
type SortItems = VDataTable['sortBy']
|
||||
|
||||
const headers: ReadonlyHeaders = [
|
||||
{ title: 'ID', align: 'start', sortable: true, key: 'id' },
|
||||
{ title: 'Character', align: 'start', sortable: true, key: 'characterName' },
|
||||
{ title: 'Role', align: 'start', sortable: true, key: 'role' },
|
||||
{ title: 'Player', align: 'start', sortable: true, key: 'playerName' },
|
||||
{ title: 'Status', align: 'start', sortable: true, key: 'status' },
|
||||
{
|
||||
title: 'Creation Date',
|
||||
align: 'start',
|
||||
sortable: true,
|
||||
key: 'creationDate'
|
||||
}
|
||||
]
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const characters = ref<Character[]>([])
|
||||
const pageCount = ref(1)
|
||||
const resultePerPage = ref(10)
|
||||
|
||||
const count = ref(10)
|
||||
|
||||
const pageValue = ref(1)
|
||||
const filterValue = ref('')
|
||||
const sortValue = ref<SortItems>([])
|
||||
|
||||
async function loadData({ page, itemsPerPage, sortBy }: any) {
|
||||
let sortString = 'id asc'
|
||||
if (sortBy[0]) {
|
||||
sortString = `${sortBy[0].key} ${sortBy[0].order}`
|
||||
}
|
||||
|
||||
const response = await axios.post('/api/character', {
|
||||
page: `${page - 1}`,
|
||||
count: `${itemsPerPage}`,
|
||||
orderBy: sortString,
|
||||
filter: filterValue.value ? `characterName:"${filterValue.value}"` : ''
|
||||
})
|
||||
|
||||
characters.value = response.data.characterData
|
||||
pageCount.value = response.data.pageCount
|
||||
count.value = response.data.totalCount
|
||||
|
||||
router.replace({
|
||||
query: {
|
||||
page: pageValue.value,
|
||||
sort: sortString === 'id asc' ? '' : sortString,
|
||||
filter: filterValue.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
loadData({
|
||||
page: pageValue.value,
|
||||
itemsPerPage: 10,
|
||||
sortBy: sortValue.value
|
||||
})
|
||||
}
|
||||
|
||||
function buildSortFromQuery(query: string | LocationQueryValue[]): SortItems {
|
||||
if (typeof route.query.sort === 'string') {
|
||||
const key = route.query.sort.split(' ')[0]
|
||||
const order = route.query.sort.split(' ')[1] === 'asc' ? 'asc' : 'desc'
|
||||
return [{ key, order }]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
watch(route, (newRoute, oldRoute) => {
|
||||
if (newRoute.query.page) {
|
||||
pageValue.value = Number(newRoute.query.page)
|
||||
}
|
||||
|
||||
if (newRoute.query.sort) {
|
||||
sortValue.value = buildSortFromQuery(newRoute.query.sort)
|
||||
}
|
||||
|
||||
if (newRoute.query.filter) {
|
||||
filterValue.value = newRoute.query.filter.toString()
|
||||
}
|
||||
})
|
||||
|
||||
let debounce: ReturnType<typeof setTimeout>
|
||||
watch(filterValue, (newFilter: string, oldFilter: string) => {
|
||||
clearTimeout(debounce)
|
||||
debounce = setTimeout(() => {
|
||||
router.replace({ query: { page: route.query.page, filter: newFilter } })
|
||||
}, 500)
|
||||
loadData({
|
||||
page: pageValue.value,
|
||||
itemsPerPage: resultePerPage.value,
|
||||
sortBy: []
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeMount(async () => {
|
||||
// Initialize Filter
|
||||
if (!route.query.page) {
|
||||
pageValue.value = 1
|
||||
} else {
|
||||
pageValue.value = Number(route.query.page)
|
||||
count.value = Number(route.query.page) * 10
|
||||
}
|
||||
|
||||
// Initialize Sort
|
||||
if (!route.query.sort) {
|
||||
sortValue.value = []
|
||||
} else {
|
||||
sortValue.value = buildSortFromQuery(route.query.sort)
|
||||
}
|
||||
|
||||
// Initialize Filter
|
||||
if (route.query.filter) {
|
||||
filterValue.value = route.query.filter.toString()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
17
app/frontend/src/vues/DmList.vue
Normal file
17
app/frontend/src/vues/DmList.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>Hello World</template>
|
||||
|
||||
<style></style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DmStats } from '../types'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { VDataTable } from 'vuetify/components'
|
||||
|
||||
type ReadonlyHeaders = VDataTable['$props']['headers']
|
||||
|
||||
const headers: ReadonlyHeaders = [
|
||||
{ title: 'Game Master', align: 'start', sortable: true, key: 'id' },
|
||||
{ title: 'Game Count', align: 'start', sortable: true, key: 'characterName' },
|
||||
{ title: 'Last Game', align: 'start', sortable: true, key: 'role' }
|
||||
]
|
||||
</script>
|
||||
158
app/frontend/src/vues/GameDetails.vue
Normal file
158
app/frontend/src/vues/GameDetails.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<h1>#{{ game.id }} - {{ game.title }}</h1>
|
||||
<h2>GameMaster: {{ game.gamemaster }}</h2>
|
||||
<div class="container">
|
||||
<div class="d-flex flex-row">
|
||||
<div class="result-box">
|
||||
<h3>Picks</h3>
|
||||
<div v-for="pick in picks">
|
||||
{{ pick.pickedCharacter!.playerName }} as {{ pick.characterName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-box">
|
||||
<h3>Payout</h3>
|
||||
<div>{{ game.payoutEB }} EB</div>
|
||||
<div>{{ game.payoutIP }} IP</div>
|
||||
<div>{{ game.payoutLoot }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="d-flex flex-row">
|
||||
<div class="character-list">
|
||||
<h3>{{ picks.length }} Character Picks</h3>
|
||||
<v-data-table-virtual
|
||||
:headers="pickHeaders"
|
||||
:items="picks"
|
||||
height="500px"
|
||||
fixed-header
|
||||
>
|
||||
<template v-slot:item.characterId="{ item }">
|
||||
<a v-bind:href="`/#/characters/${item.characterId}`">{{
|
||||
item.characterId
|
||||
}}</a>
|
||||
</template>
|
||||
<template v-slot:item.characterName="{ item }">
|
||||
<a v-bind:href="`/#/characters/${item.characterId}`">{{
|
||||
item.characterName
|
||||
}}</a>
|
||||
</template>
|
||||
</v-data-table-virtual>
|
||||
</div>
|
||||
<div class="character-list">
|
||||
<h3>{{ apps.length }} Character Apps</h3>
|
||||
<v-data-table-virtual
|
||||
:headers="appHeaders"
|
||||
:items="apps"
|
||||
height="500px"
|
||||
fixed-header
|
||||
>
|
||||
<template v-slot:item.characterId="{ item }">
|
||||
<a v-bind:href="`/#/characters/${item.characterId}`">{{
|
||||
item.characterId
|
||||
}}</a>
|
||||
</template>
|
||||
<template v-slot:item.characterName="{ item }">
|
||||
<a v-bind:href="`/#/characters/${item.characterId}`">{{
|
||||
item.characterName
|
||||
}}</a>
|
||||
</template>
|
||||
</v-data-table-virtual>
|
||||
</div>
|
||||
</div>
|
||||
</div></v-container
|
||||
>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.game-title-box {
|
||||
margin-top: 25px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.gm-title-box {
|
||||
margin-top: 5px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
margin: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.character-list {
|
||||
margin: 20px;
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Game, Character, GameCharacter } from '../types'
|
||||
import { watch, ref } from 'vue'
|
||||
import { VDataTable } from 'vuetify/components'
|
||||
import { useRoute } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
|
||||
type ReadonlyHeaders = VDataTable['$props']['headers']
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const pickHeaders: ReadonlyHeaders = [
|
||||
{ title: 'ID', align: 'start', key: 'characterId' },
|
||||
{ title: 'Character', align: 'start', key: 'characterName' },
|
||||
{ title: 'Status', align: 'start', key: 'pickedCharacter.status' },
|
||||
{ title: 'Player', align: 'start', key: 'pickedCharacter.playerName' }
|
||||
]
|
||||
|
||||
const appHeaders: ReadonlyHeaders = [
|
||||
{ title: 'ID', align: 'start', key: 'characterId' },
|
||||
{ title: 'Character', align: 'start', key: 'characterName' },
|
||||
{ title: 'Status', align: 'start', key: 'appliedCharacter.status' },
|
||||
{ title: 'Player', align: 'start', key: 'appliedCharacter.playerName' }
|
||||
]
|
||||
|
||||
const gameId = ref(route.params.gameId)
|
||||
const game = ref<Game>({
|
||||
id: 0,
|
||||
title: '',
|
||||
gamemaster: '',
|
||||
payoutEB: 0,
|
||||
payoutIP: 0,
|
||||
payoutLoot: '',
|
||||
status: '',
|
||||
postdate: 0
|
||||
})
|
||||
const picks = ref<GameCharacter[]>([])
|
||||
const apps = ref<GameCharacter[]>([])
|
||||
|
||||
loadGameDetails()
|
||||
|
||||
watch(
|
||||
() => route.params.gameId,
|
||||
(newId, oldId) => {
|
||||
gameId.value = newId
|
||||
loadGameDetails()
|
||||
}
|
||||
)
|
||||
|
||||
async function loadGameDetails() {
|
||||
const response = await axios.get(`/api/game/${gameId.value}`)
|
||||
game.value = response.data
|
||||
loadPicks()
|
||||
loadApps()
|
||||
}
|
||||
|
||||
async function loadPicks() {
|
||||
const response = await axios.post(`/api/game/${gameId.value}/picks`)
|
||||
picks.value = response.data
|
||||
}
|
||||
|
||||
async function loadApps() {
|
||||
const response = await axios.post(`/api/game/${gameId.value}/apps`)
|
||||
apps.value = response.data
|
||||
}
|
||||
</script>
|
||||
166
app/frontend/src/vues/GameList.vue
Normal file
166
app/frontend/src/vues/GameList.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="d-flex flex-row">
|
||||
<v-text-field
|
||||
class="filter flex-1-0"
|
||||
placeholder="Filter by Name"
|
||||
v-model="filterValue"
|
||||
></v-text-field>
|
||||
</div>
|
||||
<v-data-table-server
|
||||
:headers="headers"
|
||||
:items="games"
|
||||
:items-length="count"
|
||||
v-model:items-per-page="resultsPerPage"
|
||||
v-model:page="pageValue"
|
||||
v-model:sort-by="sortValue"
|
||||
@update:options="loadData"
|
||||
>
|
||||
<template v-slot:item.id="{ item }">
|
||||
<router-link :to="{ name: 'game', params: { gameId: item.id } }">{{
|
||||
item.id
|
||||
}}</router-link>
|
||||
</template>
|
||||
<template v-slot:item.title="{ item }">
|
||||
<router-link :to="{ name: 'game', params: { gameId: item.id } }">{{
|
||||
item.title
|
||||
}}</router-link>
|
||||
</template>
|
||||
<template v-slot:item.postdate="{ item }">
|
||||
{{ new Date(item.postdate * 1000).toISOString().split('T')[0] }}
|
||||
</template>
|
||||
|
||||
<template #bottom></template>
|
||||
</v-data-table-server>
|
||||
<v-pagination
|
||||
v-model:model-value="pageValue"
|
||||
:length="pageCount"
|
||||
></v-pagination>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.filter {
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Game } from '../types'
|
||||
import GameTable from './GameTable.vue'
|
||||
import { onBeforeMount, watch, ref } from 'vue'
|
||||
import { useRoute, useRouter, LocationQueryValue } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import { VDataTable } from 'vuetify/components'
|
||||
|
||||
type ReadonlyHeaders = VDataTable['$props']['headers']
|
||||
type SortItems = VDataTable['sortBy']
|
||||
|
||||
const headers: ReadonlyHeaders = [
|
||||
{ title: 'ID', align: 'start', sortable: true, key: 'id' },
|
||||
{ title: 'Title', align: 'start', sortable: true, key: 'title' },
|
||||
{ title: 'Status', align: 'start', sortable: true, key: 'status' },
|
||||
{ title: 'Post Date', align: 'start', sortable: true, key: 'postdate' }
|
||||
]
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const games = ref<Game[]>([])
|
||||
const pageCount = ref(1)
|
||||
const resultsPerPage = ref(10)
|
||||
|
||||
const count = ref(0)
|
||||
|
||||
const pageValue = ref(1)
|
||||
const filterValue = ref('')
|
||||
const sortValue = ref<SortItems>([])
|
||||
|
||||
async function loadData({ page, itemsPerPage, sortBy }: any) {
|
||||
let sortString = 'id asc'
|
||||
if (sortBy[0]) {
|
||||
sortString = `${sortBy[0].key} ${sortBy[0].order}`
|
||||
}
|
||||
|
||||
const response = await axios.post('/api/game', {
|
||||
page: `${page - 1}`,
|
||||
count: `${itemsPerPage}`,
|
||||
orderBy: sortString,
|
||||
filter: filterValue.value ? `title:"${filterValue.value}"` : ''
|
||||
})
|
||||
|
||||
games.value = response.data.gameData
|
||||
pageCount.value = response.data.pageCount
|
||||
count.value = response.data.totalCount
|
||||
|
||||
router.replace({
|
||||
query: {
|
||||
page: pageValue.value,
|
||||
sort: sortString === 'id asc' ? '' : sortString,
|
||||
filter: filterValue.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
loadData({
|
||||
page: pageValue.value,
|
||||
itemsPerPage: 10,
|
||||
sortBy: sortValue.value
|
||||
})
|
||||
}
|
||||
|
||||
function buildSortFromQuery(query: string | LocationQueryValue[]): SortItems {
|
||||
if (typeof route.query.sort === 'string') {
|
||||
const key = route.query.sort.split(' ')[0]
|
||||
const order = route.query.sort.split(' ')[1] === 'asc' ? 'asc' : 'desc'
|
||||
return [{ key, order }]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
watch(route, (newRoute, oldRoute) => {
|
||||
if (newRoute.query.page) {
|
||||
pageValue.value = Number(newRoute.query.page)
|
||||
}
|
||||
|
||||
if (newRoute.query.sort) {
|
||||
sortValue.value = buildSortFromQuery(newRoute.query.sort)
|
||||
}
|
||||
|
||||
if (newRoute.query.filter) {
|
||||
filterValue.value = newRoute.query.filter.toString()
|
||||
}
|
||||
})
|
||||
|
||||
let debounce: ReturnType<typeof setTimeout>
|
||||
watch(filterValue, (newFilter: string, oldFilter: string) => {
|
||||
clearTimeout(debounce)
|
||||
debounce = setTimeout(() => {
|
||||
reloadData()
|
||||
}, 500)
|
||||
})
|
||||
|
||||
onBeforeMount(async () => {
|
||||
// Initialize Filter
|
||||
if (!route.query.page) {
|
||||
pageValue.value = 1
|
||||
} else {
|
||||
pageValue.value = Number(route.query.page)
|
||||
count.value = Number(route.query.page) * 10
|
||||
}
|
||||
|
||||
// Initialize Sort
|
||||
if (!route.query.sort) {
|
||||
sortValue.value = []
|
||||
} else {
|
||||
sortValue.value = buildSortFromQuery(route.query.sort)
|
||||
}
|
||||
|
||||
// Initialize Filter
|
||||
if (route.query.filter) {
|
||||
filterValue.value = route.query.filter.toString()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
41
app/frontend/src/vues/GameTable.vue
Normal file
41
app/frontend/src/vues/GameTable.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<v-data-table-virtual
|
||||
:headers="headers"
|
||||
:items="gameList"
|
||||
height="500px"
|
||||
fixed-header
|
||||
>
|
||||
<template v-slot:item.id="{ item }">
|
||||
<a v-bind:href="`/#/games/${item.id}`">{{ item.id }}</a>
|
||||
</template>
|
||||
<template v-slot:item.title="{ item }">
|
||||
<a v-bind:href="`/#/games/${item.id}`">{{ item.title }}</a>
|
||||
</template>
|
||||
<template v-slot:item.postdate="{ item }">
|
||||
{{ new Date(item.postdate * 1000).toISOString().split('T')[0] }}
|
||||
</template>
|
||||
</v-data-table-virtual>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Game } from '../types'
|
||||
import { defineProps } from 'vue'
|
||||
import { VDataTable } from 'vuetify/components'
|
||||
|
||||
type ReadonlyHeaders = VDataTable['$props']['headers']
|
||||
|
||||
interface Props {
|
||||
gameList: Game[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const headers: ReadonlyHeaders = [
|
||||
{ title: 'ID', align: 'start', key: 'id' },
|
||||
{ title: 'Title', align: 'start', key: 'title' },
|
||||
{ title: 'Status', align: 'start', key: 'status' },
|
||||
{ title: 'Post Date', align: 'start', key: 'postdate' }
|
||||
]
|
||||
</script>
|
||||
294
app/frontend/src/vues/ServerStats.vue
Normal file
294
app/frontend/src/vues/ServerStats.vue
Normal file
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<div class="d-flex flex-column">
|
||||
<v-select
|
||||
class="date-selector"
|
||||
v-model="dateSelect"
|
||||
variant="underlined"
|
||||
:items="dateItems"
|
||||
>
|
||||
</v-select>
|
||||
<div class="stat-bar d-flex flex-row justify-space-around">
|
||||
<div class="d-flex flex-column">
|
||||
<v-label class="title">Completed Games</v-label>
|
||||
<v-label>{{ gameStats.Complete }}</v-label>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<v-label class="title">Postponed Games</v-label>
|
||||
<v-label>{{ gameStats.Postponed }}</v-label>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<v-label class="title">Events / Fixes</v-label>
|
||||
<v-label>{{ gameStats.Events }} / {{ gameStats.Fixes }}</v-label>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<v-label class="title">Total / Average EB</v-label>
|
||||
<v-label
|
||||
>{{ Math.floor(gameStats.TotalEB) }} /
|
||||
{{ Math.floor(gameStats.AverageEB) }}</v-label
|
||||
>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<v-label class="title">Total / Average IP</v-label>
|
||||
<v-label
|
||||
>{{ Math.floor(gameStats.TotalIP) }} /
|
||||
{{ Math.floor(gameStats.AverageIP) }}</v-label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="role-table d-flex flex-row">
|
||||
<v-table class="flex-1-1">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Role</th>
|
||||
<th class="text-left">Characters</th>
|
||||
<th class="text-left">App Count</th>
|
||||
<th class="text-left">Games Played</th>
|
||||
<th class="text-left">Pick Rate %</th>
|
||||
<th class="text-left">% Games With Role</th>
|
||||
<th cladd="text-ledt">Apps Per Game</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(roleStat, roleName) in roleStats">
|
||||
<td>{{ roleName }}</td>
|
||||
<td class="text-left">
|
||||
{{ roleStat.active }}
|
||||
</td>
|
||||
<td class="text-left">{{ roleStat.apps }}</td>
|
||||
<td class="text-left">{{ roleStat.picks }}</td>
|
||||
<td class="text-left">
|
||||
{{ Math.floor((roleStat.picks / roleStat.apps) * 100) }}%
|
||||
</td>
|
||||
<td class="text-left">
|
||||
{{ Math.floor((roleStat.picks / gameStats.Complete) * 100) }}%
|
||||
</td>
|
||||
<td class="text-left">
|
||||
{{ Math.round((roleStat.apps / gameStats.Complete) * 100) / 100 }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
<v-sheet class="chart d-flex flex-column">
|
||||
<v-select v-model="chartSelect" :items="chartItems"></v-select>
|
||||
<canvas id="piechart"></canvas>
|
||||
<div class="flex-1-1"></div>
|
||||
</v-sheet>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.date-selector {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.graph-selector {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.stat-bar {
|
||||
padding: 10px;
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 20px;
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.role-table {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
.chart {
|
||||
margin-left: 20px;
|
||||
width: 25%;
|
||||
width: 450px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { GameStats, RoleStats } from '../types'
|
||||
import { onMounted, watch, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Chart from 'chart.js/auto'
|
||||
import axios from 'axios'
|
||||
|
||||
type ChartType = 'gametypes' | 'apps' | 'picks' | 'active'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const dateSelect = ref(-1)
|
||||
const dateItems = buildDateItems()
|
||||
const chartSelect = ref<ChartType>('active')
|
||||
const chartItems = [
|
||||
{ title: 'Game Types', value: 'gametypes' },
|
||||
{ title: 'Active Characters', value: 'active' },
|
||||
{ title: 'Role Picks', value: 'picks' },
|
||||
{ title: 'Role Applications', value: 'apps' }
|
||||
]
|
||||
const gameStats = ref<GameStats>({
|
||||
Complete: 0,
|
||||
Postponed: 0,
|
||||
Pending: 0,
|
||||
Fixes: 0,
|
||||
Events: 0,
|
||||
AverageIP: 0,
|
||||
AverageEB: 0,
|
||||
TotalIP: 0,
|
||||
TotalEB: 0
|
||||
})
|
||||
const roleStats = ref<RoleStats>({
|
||||
Fixer: {
|
||||
apps: 0,
|
||||
picks: 0,
|
||||
active: 0
|
||||
},
|
||||
Tech: {
|
||||
apps: 0,
|
||||
picks: 0,
|
||||
active: 0
|
||||
},
|
||||
Medtech: {
|
||||
apps: 0,
|
||||
picks: 0,
|
||||
active: 0
|
||||
},
|
||||
Media: {
|
||||
apps: 0,
|
||||
picks: 0,
|
||||
active: 0
|
||||
},
|
||||
Netrunner: {
|
||||
apps: 0,
|
||||
picks: 0,
|
||||
active: 0
|
||||
},
|
||||
Solo: {
|
||||
apps: 0,
|
||||
picks: 0,
|
||||
active: 0
|
||||
},
|
||||
Nomad: {
|
||||
apps: 0,
|
||||
picks: 0,
|
||||
active: 0
|
||||
},
|
||||
Exec: {
|
||||
apps: 0,
|
||||
picks: 0,
|
||||
active: 0
|
||||
},
|
||||
Lawman: {
|
||||
apps: 0,
|
||||
picks: 0,
|
||||
active: 0
|
||||
},
|
||||
Rocker: {
|
||||
apps: 0,
|
||||
picks: 0,
|
||||
active: 0
|
||||
}
|
||||
})
|
||||
|
||||
let chart: Chart<'pie', number[], string>
|
||||
|
||||
async function loadData() {
|
||||
const gameStatsResponse = await axios.post('/api/serverstats/gamestats', {
|
||||
monthId: dateSelect.value
|
||||
})
|
||||
gameStats.value = gameStatsResponse.data
|
||||
|
||||
const roleStatsResponse = await axios.post('/api/serverstats/rolestats', {
|
||||
monthId: dateSelect.value
|
||||
})
|
||||
roleStats.value = roleStatsResponse.data
|
||||
}
|
||||
|
||||
function buildDateItems() {
|
||||
const items = [{ title: 'All Time', value: -1 }]
|
||||
|
||||
const date = new Date()
|
||||
while (date.getUTCFullYear() != 2023 || date.getUTCMonth() != 0) {
|
||||
items.push(buildDateEntry(date))
|
||||
date.setUTCMonth(date.getUTCMonth() - 1)
|
||||
}
|
||||
items.push(buildDateEntry(date))
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
function buildDateEntry(date: Date) {
|
||||
const monthId = dateToMonthId(date)
|
||||
return {
|
||||
title: date.toLocaleString('en-us', { month: 'short', year: 'numeric' }),
|
||||
value: monthId
|
||||
}
|
||||
}
|
||||
|
||||
function dateToMonthId(date: Date) {
|
||||
return (date.getUTCFullYear() - 2023) * 12 + date.getUTCMonth()
|
||||
}
|
||||
|
||||
function updateChart(stats: RoleStats, tag: ChartType) {
|
||||
if (tag === 'gametypes') {
|
||||
chart.data = {
|
||||
labels: ['Standard', 'Postponed', 'Pending', 'Event', 'Fix'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Game Type',
|
||||
data: [
|
||||
gameStats.value.Complete -
|
||||
gameStats.value.Events -
|
||||
gameStats.value.Fixes,
|
||||
gameStats.value.Postponed,
|
||||
gameStats.value.Pending,
|
||||
gameStats.value.Events,
|
||||
gameStats.value.Fixes
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
} else {
|
||||
chart.data = {
|
||||
labels: Object.keys(stats),
|
||||
datasets: [
|
||||
{
|
||||
label: tag,
|
||||
data: Object.values(stats).map((p) => p[tag])
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
chart.update()
|
||||
}
|
||||
|
||||
watch(dateSelect, async (newValue, oldValue) => {
|
||||
router.replace({ query: { monthId: dateSelect.value } })
|
||||
loadData()
|
||||
})
|
||||
|
||||
watch(roleStats, async (newValue, oldValue) => {
|
||||
updateChart(newValue, chartSelect.value)
|
||||
})
|
||||
|
||||
watch(chartSelect, async (newValue: ChartType, oldValue: ChartType) => {
|
||||
updateChart(roleStats.value, newValue)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!route.query.monthId) {
|
||||
router.replace({ query: { monthId: -1 } })
|
||||
} else {
|
||||
dateSelect.value = Number(route.query.monthId)
|
||||
}
|
||||
loadData()
|
||||
chart = new Chart(document.getElementById('piechart')! as HTMLCanvasElement, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: []
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
25
app/frontend/tsconfig.json
Normal file
25
app/frontend/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
9
app/frontend/tsconfig.node.json
Normal file
9
app/frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.mts"]
|
||||
}
|
||||
140
app/frontend/typed-router.d.ts
vendored
Normal file
140
app/frontend/typed-router.d.ts
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
|
||||
// It's recommended to commit this file.
|
||||
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
||||
|
||||
/// <reference types="unplugin-vue-router/client" />
|
||||
|
||||
import type {
|
||||
// type safe route locations
|
||||
RouteLocationTypedList,
|
||||
RouteLocationResolvedTypedList,
|
||||
RouteLocationNormalizedTypedList,
|
||||
RouteLocationNormalizedLoadedTypedList,
|
||||
RouteLocationAsString,
|
||||
RouteLocationAsRelativeTypedList,
|
||||
RouteLocationAsPathTypedList,
|
||||
|
||||
// helper types
|
||||
// route definitions
|
||||
RouteRecordInfo,
|
||||
ParamValue,
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
|
||||
// vue-router extensions
|
||||
_RouterTyped,
|
||||
RouterLinkTyped,
|
||||
RouterLinkPropsTyped,
|
||||
NavigationGuard,
|
||||
UseLinkFnTyped,
|
||||
|
||||
// data fetching
|
||||
_DataLoader,
|
||||
_DefineLoaderOptions,
|
||||
} from 'unplugin-vue-router/types'
|
||||
|
||||
declare module 'vue-router/auto/routes' {
|
||||
export interface RouteNamedMap {
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'vue-router/auto' {
|
||||
import type { RouteNamedMap } from 'vue-router/auto/routes'
|
||||
|
||||
export type RouterTyped = _RouterTyped<RouteNamedMap>
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationNormalized` (the type of `to` and `from` in navigation guards).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationNormalized<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationNormalizedLoaded` (the return type of `useRoute()`).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationNormalizedLoaded<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationResolved` (the returned route of `router.resolve()`).
|
||||
* Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationResolved<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationResolvedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocation` . Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocation<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Type safe version of `RouteLocationRaw` . Allows passing the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteLocationRaw<Name extends keyof RouteNamedMap = keyof RouteNamedMap> =
|
||||
| RouteLocationAsString<RouteNamedMap>
|
||||
| RouteLocationAsRelativeTypedList<RouteNamedMap>[Name]
|
||||
| RouteLocationAsPathTypedList<RouteNamedMap>[Name]
|
||||
|
||||
/**
|
||||
* Generate a type safe params for a route location. Requires the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteParams<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['params']
|
||||
/**
|
||||
* Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic.
|
||||
*/
|
||||
export type RouteParamsRaw<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['paramsRaw']
|
||||
|
||||
export function useRouter(): RouterTyped
|
||||
export function useRoute<Name extends keyof RouteNamedMap = keyof RouteNamedMap>(name?: Name): RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
||||
|
||||
export const useLink: UseLinkFnTyped<RouteNamedMap>
|
||||
|
||||
export function onBeforeRouteLeave(guard: NavigationGuard<RouteNamedMap>): void
|
||||
export function onBeforeRouteUpdate(guard: NavigationGuard<RouteNamedMap>): void
|
||||
|
||||
export const RouterLink: RouterLinkTyped<RouteNamedMap>
|
||||
export const RouterLinkProps: RouterLinkPropsTyped<RouteNamedMap>
|
||||
|
||||
// Experimental Data Fetching
|
||||
|
||||
export function defineLoader<
|
||||
P extends Promise<any>,
|
||||
Name extends keyof RouteNamedMap = keyof RouteNamedMap,
|
||||
isLazy extends boolean = false,
|
||||
>(
|
||||
name: Name,
|
||||
loader: (route: RouteLocationNormalizedLoaded<Name>) => P,
|
||||
options?: _DefineLoaderOptions<isLazy>,
|
||||
): _DataLoader<Awaited<P>, isLazy>
|
||||
export function defineLoader<
|
||||
P extends Promise<any>,
|
||||
isLazy extends boolean = false,
|
||||
>(
|
||||
loader: (route: RouteLocationNormalizedLoaded) => P,
|
||||
options?: _DefineLoaderOptions<isLazy>,
|
||||
): _DataLoader<Awaited<P>, isLazy>
|
||||
|
||||
export {
|
||||
_definePage as definePage,
|
||||
_HasDataLoaderMeta as HasDataLoaderMeta,
|
||||
_setupDataFetchingGuard as setupDataFetchingGuard,
|
||||
_stopDataFetchingScope as stopDataFetchingScope,
|
||||
} from 'unplugin-vue-router/runtime'
|
||||
}
|
||||
|
||||
declare module 'vue-router' {
|
||||
import type { RouteNamedMap } from 'vue-router/auto/routes'
|
||||
|
||||
export interface TypesConfig {
|
||||
beforeRouteUpdate: NavigationGuard<RouteNamedMap>
|
||||
beforeRouteLeave: NavigationGuard<RouteNamedMap>
|
||||
|
||||
$route: RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[keyof RouteNamedMap]
|
||||
$router: _RouterTyped<RouteNamedMap>
|
||||
|
||||
RouterLink: RouterLinkTyped<RouteNamedMap>
|
||||
}
|
||||
}
|
||||
73
app/frontend/vite.config.ts
Normal file
73
app/frontend/vite.config.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// Plugins
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import Fonts from 'unplugin-fonts/vite'
|
||||
import Layouts from 'vite-plugin-vue-layouts'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import VueRouter from 'unplugin-vue-router/vite'
|
||||
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
|
||||
|
||||
// Utilities
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default ({mode}) => {
|
||||
process.env = {...process.env, ...loadEnv(mode, process.cwd())}
|
||||
return defineConfig({
|
||||
plugins: [
|
||||
VueRouter(),
|
||||
Layouts(),
|
||||
Vue({
|
||||
template: { transformAssetUrls },
|
||||
}),
|
||||
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
|
||||
Vuetify({
|
||||
autoImport: true,
|
||||
styles: {
|
||||
configFile: 'src/styles/settings.scss',
|
||||
},
|
||||
}),
|
||||
Components(),
|
||||
Fonts({
|
||||
google: {
|
||||
families: [ {
|
||||
name: 'Roboto',
|
||||
styles: 'wght@100;300;400;500;700;900',
|
||||
}],
|
||||
},
|
||||
}),
|
||||
AutoImport({
|
||||
imports: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
],
|
||||
dts: true,
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
},
|
||||
vueTemplate: true,
|
||||
}),
|
||||
],
|
||||
define: { 'process.env': {} },
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
extensions: [
|
||||
'.js',
|
||||
'.json',
|
||||
'.jsx',
|
||||
'.mjs',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.vue',
|
||||
],
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
"/api": "http://localhost:3001"
|
||||
}
|
||||
},
|
||||
})}
|
||||
Reference in New Issue
Block a user