commit
b93ee63584
19 changed files with 1991 additions and 0 deletions
@ -0,0 +1,30 @@
|
||||
# Logs |
||||
logs |
||||
*.log |
||||
npm-debug.log* |
||||
yarn-debug.log* |
||||
yarn-error.log* |
||||
pnpm-debug.log* |
||||
lerna-debug.log* |
||||
|
||||
node_modules |
||||
.DS_Store |
||||
dist |
||||
dist-ssr |
||||
coverage |
||||
*.local |
||||
|
||||
/cypress/videos/ |
||||
/cypress/screenshots/ |
||||
|
||||
# Editor directories and files |
||||
.vscode/* |
||||
!.vscode/extensions.json |
||||
.idea |
||||
*.suo |
||||
*.ntvs* |
||||
*.njsproj |
||||
*.sln |
||||
*.sw? |
||||
|
||||
*.tsbuildinfo |
||||
@ -0,0 +1,23 @@
|
||||
FROM node:20-alpine as build-stage |
||||
|
||||
WORKDIR /app |
||||
|
||||
COPY . . |
||||
|
||||
RUN npm install |
||||
|
||||
RUN npm run build |
||||
|
||||
# ######################### |
||||
FROM nginx as production-stage |
||||
|
||||
# Copy dependencies from builder |
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html |
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh |
||||
RUN chmod +x /docker-entrypoint.sh |
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf |
||||
|
||||
EXPOSE 3020 |
||||
|
||||
ENTRYPOINT "/docker-entrypoint.sh" |
||||
@ -0,0 +1,29 @@
|
||||
# suivi-loot-wow |
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. |
||||
|
||||
## Recommended IDE Setup |
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). |
||||
|
||||
## Customize configuration |
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/). |
||||
|
||||
## Project Setup |
||||
|
||||
```sh |
||||
npm install |
||||
``` |
||||
|
||||
### Compile and Hot-Reload for Development |
||||
|
||||
```sh |
||||
npm run dev |
||||
``` |
||||
|
||||
### Compile and Minify for Production |
||||
|
||||
```sh |
||||
npm run build |
||||
``` |
||||
@ -0,0 +1,10 @@
|
||||
# !/bin/sh |
||||
|
||||
ROOT_DIR=/usr/share/nginx/html |
||||
|
||||
for file in $ROOT_DIR/*.js $ROOT_DIR/index.html $ROOT_DIR/assets/*.js*; |
||||
do |
||||
sed -i 's|VITE_SERVER_URL_PLACEHOLDER|'${VITE_SERVER_SUIVI_URL}'|g' $file |
||||
done |
||||
|
||||
nginx -g 'daemon off;' |
||||
@ -0,0 +1,15 @@
|
||||
<!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>Suivi Loots</title> |
||||
</head> |
||||
<body> |
||||
<div id="app"></div> |
||||
<script>const whTooltips = { colorLinks: true, iconizeLinks: true, renameLinks: true };</script> |
||||
<script src="https://wow.zamimg.com/js/tooltips.js"></script> |
||||
<script type="module" src="/src/main.js"></script> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,8 @@
|
||||
{ |
||||
"compilerOptions": { |
||||
"paths": { |
||||
"@/*": ["./src/*"] |
||||
} |
||||
}, |
||||
"exclude": ["node_modules", "dist"] |
||||
} |
||||
@ -0,0 +1,19 @@
|
||||
gzip on; |
||||
gzip_disable "msie6"; |
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml font/ttf font/woff2 font/woff font/opentype image/png; |
||||
|
||||
|
||||
server { |
||||
listen 3020; |
||||
listen [::]:3020; |
||||
|
||||
root /usr/share/nginx/html; |
||||
|
||||
index index.html; |
||||
|
||||
resolver 127.0.0.1; |
||||
|
||||
location / { |
||||
try_files $uri $uri/ /index.html; |
||||
} |
||||
} |
||||
@ -0,0 +1,22 @@
|
||||
{ |
||||
"name": "suivi-loot-wow", |
||||
"version": "0.0.0", |
||||
"private": true, |
||||
"type": "module", |
||||
"author": "Shalma <sebcas@yahoo.fr>", |
||||
"scripts": { |
||||
"dev": "vite --host", |
||||
"build": "vite build", |
||||
"preview": "vite preview" |
||||
}, |
||||
"dependencies": { |
||||
"luxon": "^3.4.4", |
||||
"vue": "^3.4.29", |
||||
"vuetify": "^3.6.13" |
||||
}, |
||||
"devDependencies": { |
||||
"@mdi/font": "^7.4.47", |
||||
"@vitejs/plugin-vue": "^5.0.5", |
||||
"vite": "^5.3.1" |
||||
} |
||||
} |
||||
|
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,117 @@
|
||||
<script setup> |
||||
import { ref, onMounted, onBeforeUnmount, computed } from 'vue' |
||||
import BisList from '@/components/BisList.vue' |
||||
import HistoryList from '@/components/HistoryList.vue' |
||||
import RosterList from '@/components/RosterList.vue' |
||||
import { httpRequest } from '@/plugins/httpRequest.js' |
||||
|
||||
const tab = ref('bis-list') |
||||
|
||||
const loginDialog = ref(false) |
||||
const username = ref('') |
||||
const password = ref('') |
||||
const errorMessage = ref('') |
||||
|
||||
onMounted(() => { |
||||
document.addEventListener('keypress', eventKey) |
||||
}) |
||||
|
||||
onBeforeUnmount(() => { |
||||
document.removeEventListener('keypress', eventKey) |
||||
}) |
||||
|
||||
const eventKey = async (e) => { |
||||
if (e.keyCode === 13) { |
||||
e.preventDefault(); |
||||
await submitLogin() |
||||
} |
||||
} |
||||
|
||||
const isLogged = computed(() => { |
||||
if (localStorage.getItem('token')) return true |
||||
return false |
||||
}) |
||||
|
||||
const login = () => { |
||||
loginDialog.value = true |
||||
} |
||||
|
||||
const cancelLogin = () => { |
||||
loginDialog.value = false |
||||
username.value = '' |
||||
password.value = '' |
||||
errorMessage.value = '' |
||||
} |
||||
|
||||
const submitLogin = async () => { |
||||
const body = { |
||||
username: username.value, |
||||
password: password.value |
||||
} |
||||
try { |
||||
const result = await httpRequest('/signin', { method: 'POST', body }, false, true) |
||||
if (result?.success) { |
||||
localStorage.setItem('token', result.token) |
||||
cancelLogin() |
||||
location.reload() |
||||
} else errorMessage.value = 'Bar user or password' |
||||
} catch (e) { |
||||
errorMessage.value = e.message |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<v-app> |
||||
<v-main> |
||||
<v-icon v-if="!isLogged" class="icon-login" icon="mdi-login" @click="login" /> |
||||
<v-tabs v-model="tab" align-tabs="center" color="primary"> |
||||
<v-tab value="bis-list">Bis List</v-tab> |
||||
<v-tab value="history">Historique</v-tab> |
||||
<v-tab value="roster">Roster</v-tab> |
||||
</v-tabs> |
||||
<v-tabs-window v-model="tab"> |
||||
<v-tabs-window-item value="bis-list"> |
||||
<bis-list v-if="tab === 'bis-list'" /> |
||||
</v-tabs-window-item> |
||||
<v-tabs-window-item value="history"> |
||||
<history-list v-if="tab === 'history'" /> |
||||
</v-tabs-window-item> |
||||
<v-tabs-window-item value="roster"> |
||||
<roster-list v-if="tab === 'roster'" /> |
||||
</v-tabs-window-item> |
||||
</v-tabs-window> |
||||
|
||||
<v-dialog max-width="400" persistent v-model="loginDialog"> |
||||
<v-card title="Connection"> |
||||
<v-card-text> |
||||
<v-form> |
||||
<v-container> |
||||
<v-row> |
||||
<v-col cols="12"> |
||||
<v-text-field v-model="username" label="Identifiant" density="compact" hide-details required /> |
||||
</v-col> |
||||
</v-row> |
||||
<v-row> |
||||
<v-col cols="12"> |
||||
<v-text-field v-model="password" type="password" label="Mot de passe" density="compact" hide-details required /> |
||||
</v-col> |
||||
</v-row> |
||||
<v-row> |
||||
<v-col cols="12"> |
||||
<span class="text-error">{{ errorMessage }}</span> |
||||
</v-col> |
||||
</v-row> |
||||
</v-container> |
||||
</v-form> |
||||
</v-card-text> |
||||
<v-card-actions> |
||||
<v-spacer /> |
||||
<v-btn prepend-icon="mdi-cancel" text="Annuler" variant="outlined" color="warning" @click="cancelLogin" /> |
||||
<v-btn prepend-icon="mdi-login" text="Se connecter" variant="outlined" color="primary" @click="submitLogin" /> |
||||
</v-card-actions> |
||||
</v-card> |
||||
</v-dialog> |
||||
</v-main> |
||||
</v-app> |
||||
</template> |
||||
@ -0,0 +1,67 @@
|
||||
html { |
||||
overflow: hidden; |
||||
} |
||||
|
||||
.color-DEATHKNIGHT { |
||||
color: #C41F3B; |
||||
} |
||||
|
||||
.color-DRUID { |
||||
color: #FF7D0A; |
||||
} |
||||
|
||||
.color-SHAMAN { |
||||
color: #0070DE; |
||||
} |
||||
|
||||
.color-MAGE { |
||||
color: #40C7EB; |
||||
} |
||||
|
||||
.color-HUNTER { |
||||
color: #A9D271; |
||||
} |
||||
|
||||
.color-PRIEST { |
||||
color: #FFFFFF; |
||||
} |
||||
|
||||
.color-WARRIOR { |
||||
color: #C79C6E; |
||||
} |
||||
|
||||
.color-PALADIN { |
||||
color: #F58CBA; |
||||
} |
||||
|
||||
.color-WARLOCK { |
||||
color: #8787ED; |
||||
} |
||||
|
||||
.color-ROGUE { |
||||
color: #FFF569; |
||||
} |
||||
|
||||
.pointer { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.datatable-height { |
||||
max-height: 95dvh; |
||||
} |
||||
|
||||
a, a:active, a:focus, a:hover { |
||||
color: #9E9EFF; |
||||
} |
||||
|
||||
.select-row { |
||||
background-color: #444; |
||||
} |
||||
|
||||
.icon-login { |
||||
position: absolute; |
||||
right: 30px; |
||||
top: 20px; |
||||
cursor: pointer; |
||||
z-index: 1; |
||||
} |
||||
@ -0,0 +1,142 @@
|
||||
<script setup> |
||||
import { ref, reactive, onMounted } from 'vue' |
||||
import { httpRequest } from '@/plugins/httpRequest.js' |
||||
|
||||
const itemsPerPage = ref(40) |
||||
|
||||
const bisList = reactive([]) |
||||
const selected = ref([]) |
||||
|
||||
const headers = [ |
||||
{ value: 'name', title: 'Membre', sortable: true }, |
||||
{ key: 'tete', value: 'data.tete', title: 'Tête', align: 'center' }, |
||||
{ key: 'cou', value: 'data.cou', title: 'Cou', align: 'center' }, |
||||
{ key: 'epaules', value: 'data.epaules', title: 'Épaules', align: 'center' }, |
||||
{ key: 'dos', value: 'data.dos', title: 'Dos', align: 'center' }, |
||||
{ key: 'torse', value: 'data.torse', title: 'Torse', align: 'center' }, |
||||
{ key: 'poignets', value: 'data.poignets', title: 'Poignets', align: 'center' }, |
||||
{ key: 'mains', value: 'data.mains', title: 'Mains', align: 'center' }, |
||||
{ key: 'taille', value: 'data.taille', title: 'Taille', align: 'center' }, |
||||
{ key: 'jambes', value: 'data.jambes', title: 'Jambes', align: 'center' }, |
||||
{ key: 'pied', value: 'data.pied', title: 'Pieds', align: 'center' }, |
||||
{ key: 'doigt1', value: 'data.doigt1', title: 'Doigt 1', align: 'center' }, |
||||
{ key: 'doigt2', value: 'data.doigt2', title: 'Doigt 2', align: 'center' }, |
||||
{ key: 'bijou1', value: 'data.bijou1', title: 'Bijou 1', align: 'center' }, |
||||
{ key: 'bijou2', value: 'data.bijou2', title: 'Bijou 2', align: 'center' }, |
||||
{ key: 'arme', value: 'data.arme', title: 'Arme', align: 'center' }, |
||||
{ key: 'mainGauche', value: 'data.mainGauche', title: 'Main Gauche', align: 'center' }, |
||||
{ key: 'relique', value: 'data.relique', title: 'Relique / Distance', align: 'center' }, |
||||
{ value: 'totalBIS', title: 'Total BIS', align: 'center' }, |
||||
{ value: 'tokenHM', title: 'Tokens HM', align: 'center' } |
||||
] |
||||
|
||||
onMounted(async () => { |
||||
await getBisList() |
||||
}) |
||||
|
||||
const getBisList = async () => { |
||||
bisList.length = 0 |
||||
const result = await httpRequest('/bisList', { method: 'GET' }, false, true) |
||||
bisList.push(...result.bis) |
||||
} |
||||
|
||||
const colorRowItem = (row) => { |
||||
const index = selected.value.findIndex(s => s.name === row.item.name && s.classe === row.item.classe && s.tokenHM === row.item.tokenHM) |
||||
if (index > -1) return { class: 'select-row' } |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<v-container fluid> |
||||
<v-data-table v-model="selected" class="datatable-height" show-select return-object :headers="headers" :items="bisList" :items-per-page="itemsPerPage" density="compact" :row-props="colorRowItem"> |
||||
<template #item.name="{ item }"> |
||||
<span :class="`color-${item.classe}`">{{ item.name }}</span> |
||||
</template> |
||||
<template #item.tete="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.cou="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.epaules="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.dos="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.torse="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.poignets="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.mains="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.taille="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.jambes="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.pied="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.doigt1="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.doigt2="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.bijou1="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.bijou2="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.arme="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.mainGauche="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
<template #item.relique="{ value }"> |
||||
<v-icon v-if="value === 0" icon="mdi-close" color="error" /> |
||||
<v-icon v-else icon="mdi-check" color="success" /> |
||||
<span v-if="value > 1">{{ value }}</span> |
||||
</template> |
||||
</v-data-table> |
||||
</v-container> |
||||
</template> |
||||
@ -0,0 +1,177 @@
|
||||
<script setup> |
||||
import { ref, reactive, onMounted, computed } from 'vue' |
||||
import { httpRequest } from '@/plugins/httpRequest.js' |
||||
import { DateTime } from 'luxon' |
||||
|
||||
const itemsPerPage = ref(25) |
||||
|
||||
const addHistoryDialog = ref(false) |
||||
const history = ref('') |
||||
const errorMessage = ref('') |
||||
const formValid = ref(true) |
||||
|
||||
const historyList = reactive([]) |
||||
|
||||
const historySelected = ref([]) |
||||
|
||||
const deleteHistoryDialog = ref(false) |
||||
|
||||
const isLoading = ref(false) |
||||
|
||||
const search = ref('') |
||||
|
||||
const headers = [ |
||||
{ value: 'name', title: 'Membre', sortable: true }, |
||||
{ value: 'fullDate', title: 'Date', align: 'center', sortable: true }, |
||||
{ value: 'itemID', title: 'Item' }, |
||||
{ value: 'response', title: 'Reponse', align: 'center' }, |
||||
{ value: 'votes', title: 'Votes', align: 'center' }, |
||||
{ value: 'instance', title: 'Instance', align: 'center' }, |
||||
{ value: 'boss', title: 'Boss', align: 'center' }, |
||||
{ value: 'equipLoc', title: 'Emplacement', align: 'center' }, |
||||
{ value: 'note', title: 'Note' } |
||||
] |
||||
|
||||
onMounted(async () => { |
||||
await getHistory() |
||||
}) |
||||
|
||||
const isLogged = computed(() => { |
||||
if (localStorage.getItem('token')) return true |
||||
return false |
||||
}) |
||||
|
||||
const getHistory = async () => { |
||||
historyList.length = 0 |
||||
const result = await httpRequest('/histories', { method: 'GET' }, false, true) |
||||
historyList.push(...result.histories) |
||||
} |
||||
|
||||
const addHistory = () => { |
||||
addHistoryDialog.value = true |
||||
} |
||||
|
||||
const cancelAddHistory = () => { |
||||
history.value = '' |
||||
errorMessage.value = '' |
||||
addHistoryDialog.value = false |
||||
} |
||||
|
||||
const submitAddHistory = async () => { |
||||
errorMessage.value = '' |
||||
formValid.value = true |
||||
if (!history.value || history.value === '') { |
||||
errorMessage.value = 'Le champ ne peut être vide.' |
||||
formValid.value = false |
||||
} |
||||
if (formValid.value) { |
||||
const body = { |
||||
history: history.value |
||||
} |
||||
try { |
||||
isLoading.value = true |
||||
const result = await httpRequest('/histories', { method: 'POST', body }, true, true) |
||||
if (result.success) { |
||||
await getHistory() |
||||
cancelAddHistory() |
||||
} else errorMessage.value = result.message |
||||
isLoading.value = false |
||||
} catch (e) { |
||||
errorMessage.value = e.message |
||||
isLoading.value = false |
||||
} |
||||
} |
||||
} |
||||
|
||||
const deleteHistory = () => { |
||||
deleteHistoryDialog.value = true |
||||
} |
||||
|
||||
const cancelDeleteHistory = () => { |
||||
deleteHistoryDialog.value = false |
||||
historySelected.value = [] |
||||
} |
||||
|
||||
const submitDeleteHistory = async () => { |
||||
if (historySelected.value.length > 0) { |
||||
const histories = [] |
||||
for (const hist of historySelected.value) { |
||||
histories.push(hist.id) |
||||
} |
||||
try { |
||||
isLoading.value = true |
||||
const result = await httpRequest( '/deleteHistories', { method: 'POST', body: { histories } }, true, true) |
||||
if (result.success) { |
||||
await getHistory() |
||||
cancelDeleteHistory() |
||||
} else errorMessage.value = result.message |
||||
isLoading.value = false |
||||
} catch (e) { |
||||
errorMessage.value = e.message |
||||
isLoading.value = false |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<v-data-table class="datatable-height" v-model="historySelected" filter-keys="name" :search="search" :headers="headers" :items="historyList" :items-per-page="itemsPerPage" density="compact" return-object show-select> |
||||
<template #top> |
||||
<v-toolbar flat> |
||||
<v-text-field v-model="search" class="ml-5" label="Filtrer un membre" prepend-inner-icon="mdi-magnify" variant="outlined" hide-details single-line density="compact" /> |
||||
<v-spacer /> |
||||
<v-btn v-if="isLogged" class="mr-5" :disabled="historySelected.length === 0" prepend-icon="mdi-trash-can-outline" color="error" variant="flat" text="Supprimer" @click="deleteHistory" /> |
||||
<v-btn v-if="isLogged" class="mr-5" prepend-icon="mdi-plus-circle" variant="outlined" color="primary" text="Ajouter" @click="addHistory" /> |
||||
</v-toolbar> |
||||
</template> |
||||
<template #item.name="{ item }"> |
||||
<span :class="`color-${item.class}`">{{ item.name }}</span> |
||||
</template> |
||||
<template #item.fullDate="{ value }"> |
||||
{{ DateTime.fromMillis(value).toUTC().toFormat('dd/MM/yyyy HH:mm') }} |
||||
</template> |
||||
<template #item.itemID="{ item }"> |
||||
<a :href="`https://wowhead.com/cata/fr/item=${item.itemID}`" target="_blank">{{ item.itemName }}</a> |
||||
</template> |
||||
</v-data-table> |
||||
|
||||
<v-dialog max-width="500" persistent v-model="addHistoryDialog"> |
||||
<v-card title="Ajout d'historique"> |
||||
<v-card-text> |
||||
<v-form> |
||||
<v-container> |
||||
<v-row> |
||||
<v-col cols="12"> |
||||
<v-textarea v-model="history" rows="15" bg-color="white" clearable label="Historique" variant="outlined" no-resize /> |
||||
</v-col> |
||||
</v-row> |
||||
<v-row> |
||||
<v-col cols="12"> |
||||
<span class="text-error">{{ errorMessage }}</span> |
||||
</v-col> |
||||
</v-row> |
||||
</v-container> |
||||
</v-form> |
||||
</v-card-text> |
||||
<v-card-actions> |
||||
<v-spacer /> |
||||
<v-btn prepend-icon="mdi-cancel" text="Annuler" variant="outlined" color="warning" @click="cancelAddHistory" /> |
||||
<v-btn :loading="isLoading" prepend-icon="mdi-content-save-plus-outline" text="Enregistrer" variant="outlined" color="primary" @click="submitAddHistory" /> |
||||
</v-card-actions> |
||||
</v-card> |
||||
</v-dialog> |
||||
|
||||
<v-dialog max-width="400" persistent v-model="deleteHistoryDialog"> |
||||
<v-card title="Suppression d'historique"> |
||||
<v-card-text v-if="historySelected.length"> |
||||
Êtes vous sûr de vouloir supprimer ces {{ historySelected.length }} historiques ? Cette action est irreversible. |
||||
<span class="text-error">{{ errorMessage }}</span> |
||||
</v-card-text> |
||||
<v-card-actions> |
||||
<v-spacer /> |
||||
<v-btn prepend-icon="mdi-cancel" text="Annuler" variant="outlined" color="warning" @click="cancelDeleteHistory" /> |
||||
<v-btn :loading="isLoading" prepend-icon="mdi-content-save-plus-outline" text="Confirmer" variant="outlined" color="primary" @click="submitDeleteHistory" /> |
||||
</v-card-actions> |
||||
</v-card> |
||||
</v-dialog> |
||||
</template> |
||||
@ -0,0 +1,236 @@
|
||||
<script setup> |
||||
import { ref, reactive, computed, onMounted } from 'vue' |
||||
import { httpRequest } from '@/plugins/httpRequest.js' |
||||
|
||||
const itemsPerPage = ref(40) |
||||
const addMemberDialog = ref(false) |
||||
|
||||
const rosterList = reactive([]) |
||||
|
||||
const player = ref('') |
||||
const errorMessagePlayer = ref('') |
||||
const classe = ref(null) |
||||
const errorMessageClasse = ref('') |
||||
const role = ref(null) |
||||
const errorMessageRole = ref('') |
||||
const formValid = ref(true) |
||||
const errorMessage = ref('') |
||||
|
||||
const itemToDelete = ref(null) |
||||
const deleteMemberDialog = ref(false) |
||||
|
||||
const classList = [ |
||||
{ title: 'Chaman', value: 'SHAMAN' }, |
||||
{ title: 'Chasseur', value: 'HUNTER' }, |
||||
{ title: 'Chevalier de la mort', value: 'DEATHKNIGHT' }, |
||||
{ title: 'Démoniste', value: 'WARLOCK' }, |
||||
{ title: 'Druide', value: 'DRUID' }, |
||||
{ title: 'Guerrier', value: 'WARRIOR' }, |
||||
{ title: 'Mage', value: 'MAGE' }, |
||||
{ title: 'Paladin', value: 'PALADIN' }, |
||||
{ title: 'Prêtre', value: 'PRIEST' }, |
||||
{ title: 'Voleur', value: 'ROGUE' } |
||||
] |
||||
|
||||
const isLogged = computed(() => { |
||||
if (localStorage.getItem('token')) return true |
||||
return false |
||||
}) |
||||
|
||||
const roleList = computed(() => { |
||||
switch (classe.value) { |
||||
case 'SHAMAN': |
||||
return ['Melee', 'Ranged', 'Healer'] |
||||
case 'PRIEST': |
||||
return ['Ranged', 'Healer'] |
||||
case 'HUNTER': |
||||
case 'WARLOCK': |
||||
case 'MAGE': |
||||
return ['Ranged'] |
||||
case 'DEATHKNIGHT': |
||||
case 'WARRIOR': |
||||
return ['Melee', 'Tank'] |
||||
case 'PALADIN': |
||||
return ['Melee', 'Tank', 'Healer'] |
||||
case 'ROGUE': |
||||
return ['Melee'] |
||||
case 'DRUID': |
||||
default: |
||||
return ['Melee', 'Tank', 'Ranged', 'Healer'] |
||||
} |
||||
}) |
||||
|
||||
const headers = [ |
||||
{ value: 'name', title: 'Membre', sortable: true }, |
||||
{ value: 'classe', title: 'Classe', sortable: true }, |
||||
{ value: 'role', title: 'Role', sortable: true }, |
||||
{ value: 'actions', title: '' } |
||||
] |
||||
|
||||
onMounted(async () => { |
||||
await getRoster() |
||||
}) |
||||
|
||||
const getRoster = async () => { |
||||
rosterList.length = 0 |
||||
const result = await httpRequest('/rosters', { method: 'GET' }, false, true) |
||||
rosterList.push(...result.rosters) |
||||
} |
||||
|
||||
const formatClasseName = (classe) => { |
||||
const item = classList.find(c => c.value === classe) |
||||
if (item) return item.title |
||||
return classe |
||||
} |
||||
|
||||
const addMember = () => { |
||||
addMemberDialog.value = true |
||||
} |
||||
|
||||
const cancelAddMember = () => { |
||||
resetAddMember() |
||||
addMemberDialog.value = false |
||||
} |
||||
|
||||
const resetAddMember = () => { |
||||
player.value = '' |
||||
classe.value = null |
||||
role.value = null |
||||
formValid.value = true |
||||
errorMessage.value = '' |
||||
errorMessagePlayer.value = '' |
||||
errorMessageClasse.value = '' |
||||
errorMessageRole.value = '' |
||||
} |
||||
|
||||
const submitAddMember = async () => { |
||||
errorMessagePlayer.value = '' |
||||
errorMessageClasse.value = '' |
||||
errorMessageRole.value = '' |
||||
formValid.value = true |
||||
if (!player.value || player.value === '') { |
||||
errorMessagePlayer.value = 'Le nom du personnage est requis.' |
||||
formValid.value = false |
||||
} |
||||
if (!classe.value || classe.value === '') { |
||||
errorMessageClasse.value = 'La classe du personnage est requise.' |
||||
formValid.value = false |
||||
} |
||||
if (!role.value || role.value === '' || !roleList.value.includes(role.value)) { |
||||
errorMessageRole.value = 'Le role du personnage est requis, et doit être valide' |
||||
formValid.value = false |
||||
} |
||||
if (formValid.value) { |
||||
const body = { |
||||
name: player.value, |
||||
classe: classe.value, |
||||
role: role.value |
||||
} |
||||
try { |
||||
const result = await httpRequest('/rosters', { method: 'POST', body }, true, true) |
||||
if (result.success) { |
||||
await getRoster() |
||||
cancelAddMember() |
||||
} else errorMessage.value = result.message |
||||
} catch (e) { |
||||
errorMessage.value = e.message |
||||
} |
||||
} |
||||
} |
||||
|
||||
const remove = (item) => { |
||||
itemToDelete.value = item |
||||
deleteMemberDialog.value = true |
||||
} |
||||
|
||||
const cancelDeleteMember = () => { |
||||
deleteMemberDialog.value = false |
||||
itemToDelete.value = null |
||||
} |
||||
|
||||
const submitDeleteMember = async () => { |
||||
if (itemToDelete.value) { |
||||
try { |
||||
const result = await httpRequest( `/rosters/${itemToDelete.value.name}`, { method: 'DELETE' }, true, true) |
||||
if (result.success) { |
||||
await getRoster() |
||||
cancelDeleteMember() |
||||
} else errorMessage.value = result.message |
||||
} catch (e) { |
||||
errorMessage.value = e.message |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<v-container> |
||||
<v-data-table class="datatable-height" :headers="headers" :items="rosterList" :items-per-page="itemsPerPage" density="compact"> |
||||
<template v-if="isLogged" #top> |
||||
<v-toolbar flat> |
||||
<v-spacer /> |
||||
<v-btn class="mr-5" prepend-icon="mdi-plus-circle" variant="outlined" color="primary" text="Ajouter" @click="addMember" /> |
||||
</v-toolbar> |
||||
</template> |
||||
<template #item.name="{ item }"> |
||||
<span :class="`color-${item.classe}`">{{ item.name }}</span> |
||||
</template> |
||||
<template #item.classe="{ value }"> |
||||
<span :class="`color-${value}`">{{ formatClasseName(value) }}</span> |
||||
</template> |
||||
<template v-if="isLogged" #item.actions="{ item }"> |
||||
<v-icon class="pointer" color="error" icon="mdi-delete-outline" @click="remove(item)" /> |
||||
</template> |
||||
</v-data-table> |
||||
</v-container> |
||||
|
||||
<v-dialog max-width="500" persistent v-model="addMemberDialog"> |
||||
<v-card title="Ajout d'un membre au roster"> |
||||
<v-card-text> |
||||
<v-form> |
||||
<v-container> |
||||
<v-row> |
||||
<v-col cols="12"> |
||||
<v-text-field v-model="player" label="Nom" density="compact" :error-messages="errorMessagePlayer" required /> |
||||
</v-col> |
||||
</v-row> |
||||
<v-row> |
||||
<v-col cols="12"> |
||||
<v-select v-model="classe" label="Classe" density="compact" :items="classList" :error-messages="errorMessageClasse" required /> |
||||
</v-col> |
||||
</v-row> |
||||
<v-row> |
||||
<v-col cols="12"> |
||||
<v-select v-model="role" label="Role" density="compact" :items="roleList" :error-messages="errorMessageRole" required /> |
||||
</v-col> |
||||
</v-row> |
||||
<v-row> |
||||
<v-col cols="12"> |
||||
<span class="text-error">{{ errorMessage }}</span> |
||||
</v-col> |
||||
</v-row> |
||||
</v-container> |
||||
</v-form> |
||||
</v-card-text> |
||||
<v-card-actions> |
||||
<v-spacer /> |
||||
<v-btn prepend-icon="mdi-cancel" text="Annuler" variant="outlined" color="warning" @click="cancelAddMember" /> |
||||
<v-btn prepend-icon="mdi-content-save-plus-outline" text="Enregistrer" variant="outlined" color="primary" @click="submitAddMember" /> |
||||
</v-card-actions> |
||||
</v-card> |
||||
</v-dialog> |
||||
|
||||
<v-dialog max-width="400" persistent v-model="deleteMemberDialog"> |
||||
<v-card title="Suppression d'un membre du roster"> |
||||
<v-card-text v-if="itemToDelete"> |
||||
Êtes vous sûr de vouloir supprimer <b>"{{ itemToDelete.name }}"</b> du roster ? Cette action est irreversible. |
||||
<span class="text-error">{{ errorMessage }}</span> |
||||
</v-card-text> |
||||
<v-card-actions> |
||||
<v-spacer /> |
||||
<v-btn prepend-icon="mdi-cancel" text="Annuler" variant="outlined" color="warning" @click="cancelDeleteMember" /> |
||||
<v-btn prepend-icon="mdi-content-save-plus-outline" text="Confirmer" variant="outlined" color="primary" @click="submitDeleteMember" /> |
||||
</v-card-actions> |
||||
</v-card> |
||||
</v-dialog> |
||||
</template> |
||||
@ -0,0 +1,28 @@
|
||||
import { createApp } from 'vue' |
||||
import App from './App.vue' |
||||
|
||||
import 'vuetify/styles' |
||||
import { createVuetify } from 'vuetify' |
||||
import * as components from 'vuetify/components' |
||||
import * as directives from 'vuetify/directives' |
||||
import { aliases, mdi } from 'vuetify/iconsets/mdi' |
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
|
||||
import './assets/main.css' |
||||
|
||||
const vuetify = createVuetify({ |
||||
theme: { |
||||
defaultTheme: 'dark' |
||||
}, |
||||
components, |
||||
directives, |
||||
icons: { |
||||
defaultSet: 'mdi', |
||||
aliases, |
||||
sets: { |
||||
mdi |
||||
} |
||||
} |
||||
}) |
||||
|
||||
createApp(App).use(vuetify).mount('#app') |
||||
@ -0,0 +1,29 @@
|
||||
const serverUrl = import.meta.env.VITE_SERVER_SUIVI_URL || 'http://localhost:3201' |
||||
|
||||
export const httpRequest = async function(url, { method = 'GET', body }, authentified = false, jsonRequest = false) { |
||||
const headers = new Headers() |
||||
if (body) { |
||||
headers.append('accept', 'application/json') |
||||
headers.append('content-type', 'application/json') |
||||
} |
||||
if (authentified) { |
||||
const token = localStorage.getItem('token') |
||||
headers.append('authorization', `Bearer ${token}`) |
||||
} |
||||
const init = { method, headers } |
||||
if (body) init.body = JSON.stringify(body); |
||||
let _url = url; |
||||
if (!_url.startsWith(serverUrl)) _url = `${serverUrl}${url}`; |
||||
const res = await fetch(_url, init); |
||||
|
||||
if (res.status === 401) { |
||||
localStorage.removeItem('token'); |
||||
// router.push({ name: 'login' });
|
||||
} else { |
||||
if (jsonRequest) { |
||||
const json = await res.json(); |
||||
if (res.ok) return json; |
||||
else return Promise.reject(json); |
||||
} else return res; |
||||
} |
||||
} |
||||
@ -0,0 +1,19 @@
|
||||
import { fileURLToPath, URL } from 'node:url' |
||||
|
||||
import { defineConfig } from 'vite' |
||||
import vue from '@vitejs/plugin-vue' |
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({ |
||||
plugins: [ |
||||
vue(), |
||||
], |
||||
server: { |
||||
port: 3200 |
||||
}, |
||||
resolve: { |
||||
alias: { |
||||
'@': fileURLToPath(new URL('./src', import.meta.url)) |
||||
} |
||||
} |
||||
}) |
||||
Loading…
Reference in new issue