build players page with React
This commit is contained in:
parent
d119b054be
commit
22f68cd9fc
|
|
@ -55,6 +55,8 @@ class PlayerController extends Controller
|
||||||
'user.player.player-name-length',
|
'user.player.player-name-length',
|
||||||
['min' => option('player_name_length_min'), 'max' => option('player_name_length_max')]
|
['min' => option('player_name_length_min'), 'max' => option('player_name_length_max')]
|
||||||
),
|
),
|
||||||
|
'score' => auth()->user()->score,
|
||||||
|
'cost' => (int) option('score_per_player'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
17
resources/assets/src/components/ViewerSkeleton.tsx
Normal file
17
resources/assets/src/components/ViewerSkeleton.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { trans } from '@/scripts/i18n'
|
||||||
|
|
||||||
|
const ViewerSkeleton: React.FC = () => (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="d-flex justify-content-between">
|
||||||
|
<h3 className="card-title">
|
||||||
|
<span>{trans('general.texturePreview')}</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-body"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ViewerSkeleton
|
||||||
33
resources/assets/src/scripts/hooks/useTexture.ts
Normal file
33
resources/assets/src/scripts/hooks/useTexture.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import * as fetch from '../net'
|
||||||
|
import { Texture, TextureType } from '../types'
|
||||||
|
|
||||||
|
type Response = fetch.ResponseBody<Texture>
|
||||||
|
|
||||||
|
export default function useTexture(): [
|
||||||
|
{ url: string; type: TextureType },
|
||||||
|
React.Dispatch<React.SetStateAction<number>>,
|
||||||
|
] {
|
||||||
|
const [tid, setTid] = useState(0)
|
||||||
|
const [url, setUrl] = useState('')
|
||||||
|
const [type, setType] = useState<TextureType>('steve')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tid <= 0) {
|
||||||
|
setUrl('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTexture = async () => {
|
||||||
|
const {
|
||||||
|
data: { hash, type },
|
||||||
|
} = await fetch.get<Response>(`/skinlib/info/${tid}`)
|
||||||
|
|
||||||
|
setUrl(`${blessing.base_url}/textures/${hash}`)
|
||||||
|
setType(type)
|
||||||
|
}
|
||||||
|
getTexture()
|
||||||
|
}, [tid])
|
||||||
|
|
||||||
|
return [{ url, type }, setTid]
|
||||||
|
}
|
||||||
|
|
@ -5,10 +5,7 @@ const virtual = document.createElement('div')
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
module: [
|
module: [() => import('../styles/home.styl'), () => import('./home-page')],
|
||||||
() => import('../styles/home.styl'),
|
|
||||||
() => import('./home-page'),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'user',
|
path: 'user',
|
||||||
|
|
@ -20,7 +17,7 @@ export default [
|
||||||
<div className="card-body"></div>
|
<div className="card-body"></div>
|
||||||
<div className="card-footer"> </div>
|
<div className="card-footer"> </div>
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'user/closet',
|
path: 'user/closet',
|
||||||
|
|
@ -29,8 +26,14 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'user/player',
|
path: 'user/player',
|
||||||
component: () => import('../views/user/Players.vue'),
|
react: () => import('../views/user/Players'),
|
||||||
el: virtual,
|
el: '#players-list',
|
||||||
|
frame: () => (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header"> </div>
|
||||||
|
<div className="card-body p-0"></div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'user/player/bind',
|
path: 'user/player/bind',
|
||||||
|
|
@ -39,9 +42,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'user/profile',
|
path: 'user/profile',
|
||||||
module: [
|
module: [() => import('../views/user/profile/index')],
|
||||||
() => import('../views/user/profile/index'),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'user/oauth/manage',
|
path: 'user/oauth/manage',
|
||||||
|
|
@ -50,9 +51,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'admin',
|
path: 'admin',
|
||||||
module: [
|
module: [() => import('../views/admin/Dashboard')],
|
||||||
() => import('../views/admin/Dashboard'),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'admin/users',
|
path: 'admin/users',
|
||||||
|
|
@ -71,9 +70,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'admin/customize',
|
path: 'admin/customize',
|
||||||
module: [
|
module: [() => import('../views/admin/Customization')],
|
||||||
() => import('../views/admin/Customization'),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'admin/i18n',
|
path: 'admin/i18n',
|
||||||
|
|
@ -92,9 +89,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'admin/update',
|
path: 'admin/update',
|
||||||
module: [
|
module: [() => import('../views/admin/Update')],
|
||||||
() => import('../views/admin/Update'),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'auth/login',
|
path: 'auth/login',
|
||||||
|
|
|
||||||
14
resources/assets/src/scripts/types.ts
Normal file
14
resources/assets/src/scripts/types.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
export type Player = {
|
||||||
|
pid: number
|
||||||
|
name: string
|
||||||
|
tid_skin: number
|
||||||
|
tid_cape: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Texture = {
|
||||||
|
tid: number
|
||||||
|
hash: string
|
||||||
|
type: TextureType
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TextureType = 'steve' | 'alex' | 'cape'
|
||||||
|
|
@ -1,292 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<portal selector="#players-list" :disabled="disablePortal">
|
|
||||||
<div class="card card-primary">
|
|
||||||
<div class="card-body table-responsive p-0">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>PID</th>
|
|
||||||
<th v-t="'general.player.player-name'" />
|
|
||||||
<th v-t="'user.player.operation'" />
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="(player, index) in players"
|
|
||||||
:key="player.pid"
|
|
||||||
class="player"
|
|
||||||
:class="{ 'player-selected': player.pid === selected }"
|
|
||||||
@click="preview(player)"
|
|
||||||
>
|
|
||||||
<td class="pid">{{ player.pid }}</td>
|
|
||||||
<td class="player-name">{{ player.name }}</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-default" @click="changeName(player)">
|
|
||||||
{{ $t('user.player.edit-pname') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-warning"
|
|
||||||
data-toggle="modal"
|
|
||||||
data-target="#modal-clear-texture"
|
|
||||||
>
|
|
||||||
{{ $t('user.player.delete-texture') }}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger" @click="deletePlayer(player, index)">
|
|
||||||
{{ $t('user.player.delete-player') }}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<button class="btn btn-primary" data-toggle="modal" data-target="#modal-add-player">
|
|
||||||
<i class="fas fa-plus" aria-hidden="true" /> {{ $t('user.player.add-player') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</portal>
|
|
||||||
|
|
||||||
<portal selector="#previewer" :disabled="disablePortal">
|
|
||||||
<previewer
|
|
||||||
v-if="using3dPreviewer"
|
|
||||||
:skin="skinUrl"
|
|
||||||
:cape="capeUrl"
|
|
||||||
:model="model"
|
|
||||||
title="user.player.player-info"
|
|
||||||
>
|
|
||||||
<template #footer>
|
|
||||||
<button class="btn btn-default" data-test="to2d" @click="togglePreviewer">
|
|
||||||
{{ $t('user.switch2dPreview') }}
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</previewer>
|
|
||||||
<div v-else class="card">
|
|
||||||
<div class="card-header card-outline">
|
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
<h3 class="card-title" v-html="$t('user.player.player-info')" />
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div id="preview-2d">
|
|
||||||
<p>
|
|
||||||
{{ $t('general.skin') }}
|
|
||||||
<a v-if="preview2d.skin" :href="`${baseUrl}/skinlib/show/${preview2d.skin}`">
|
|
||||||
<img
|
|
||||||
class="skin2d"
|
|
||||||
:src="`${baseUrl}/preview/${preview2d.skin}?height=128`"
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
<span v-else v-t="'user.player.texture-empty'" class="skin2d" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
{{ $t('general.cape') }}
|
|
||||||
<a v-if="preview2d.cape" :href="`${baseUrl}/skinlib/show/${preview2d.cape}`">
|
|
||||||
<img
|
|
||||||
class="skin2d"
|
|
||||||
:src="`${baseUrl}/preview/${preview2d.cape}?height=128`"
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
<span v-else v-t="'user.player.texture-empty'" class="skin2d" />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<button class="btn btn-default" @click="togglePreviewer">
|
|
||||||
{{ $t('user.switch3dPreview') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</portal>
|
|
||||||
|
|
||||||
<portal selector="#modals" :disabled="disablePortal">
|
|
||||||
<add-player-dialog @add="fetchPlayers" />
|
|
||||||
|
|
||||||
<modal
|
|
||||||
id="modal-clear-texture"
|
|
||||||
:title="$t('user.chooseClearTexture')"
|
|
||||||
@confirm="clearTexture"
|
|
||||||
>
|
|
||||||
<label class="form-group">
|
|
||||||
<input v-model="clear.skin" type="checkbox"> {{ $t('general.skin') }}
|
|
||||||
</label>
|
|
||||||
<br>
|
|
||||||
<label class="form-group">
|
|
||||||
<input v-model="clear.cape" type="checkbox"> {{ $t('general.cape') }}
|
|
||||||
</label>
|
|
||||||
</modal>
|
|
||||||
</portal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Modal from '../../components/Modal.vue'
|
|
||||||
import Portal from '../../components/Portal'
|
|
||||||
import AddPlayerDialog from '../../components/AddPlayerDialog.vue'
|
|
||||||
import emitMounted from '../../components/mixins/emitMounted'
|
|
||||||
import { showModal, toast } from '../../scripts/notify'
|
|
||||||
import { truthy } from '../../scripts/validators'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'Players',
|
|
||||||
components: {
|
|
||||||
AddPlayerDialog,
|
|
||||||
Modal,
|
|
||||||
Portal,
|
|
||||||
Previewer: () => import('../../components/Previewer.vue'),
|
|
||||||
},
|
|
||||||
mixins: [
|
|
||||||
emitMounted,
|
|
||||||
],
|
|
||||||
props: {
|
|
||||||
baseUrl: {
|
|
||||||
type: String,
|
|
||||||
default: blessing.base_url,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
players: [],
|
|
||||||
selected: 0,
|
|
||||||
using3dPreviewer: true,
|
|
||||||
skinUrl: '',
|
|
||||||
capeUrl: '',
|
|
||||||
model: 'steve',
|
|
||||||
preview2d: {
|
|
||||||
skin: 0,
|
|
||||||
cape: 0,
|
|
||||||
},
|
|
||||||
clear: {
|
|
||||||
skin: false,
|
|
||||||
cape: false,
|
|
||||||
},
|
|
||||||
disablePortal: process.env.NODE_ENV === 'test',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async beforeMount() {
|
|
||||||
await this.fetchPlayers()
|
|
||||||
if (this.players.length === 1) {
|
|
||||||
this.preview(this.players[0])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async fetchPlayers() {
|
|
||||||
this.players = (await this.$http.get('/user/player/list')).data
|
|
||||||
},
|
|
||||||
togglePreviewer() {
|
|
||||||
this.using3dPreviewer = !this.using3dPreviewer
|
|
||||||
},
|
|
||||||
async preview(player) {
|
|
||||||
this.selected = player.pid
|
|
||||||
|
|
||||||
this.preview2d.skin = player.tid_skin
|
|
||||||
this.preview2d.cape = player.tid_cape
|
|
||||||
|
|
||||||
if (player.tid_skin) {
|
|
||||||
const { data: skin } = await this.$http.get(`/skinlib/info/${player.tid_skin}`)
|
|
||||||
this.skinUrl = `${this.baseUrl}/textures/${skin.hash}`
|
|
||||||
this.model = skin.type
|
|
||||||
} else {
|
|
||||||
this.skinUrl = ''
|
|
||||||
this.model = 'steve'
|
|
||||||
}
|
|
||||||
if (player.tid_cape) {
|
|
||||||
const { data: cape } = await this.$http.get(`/skinlib/info/${player.tid_cape}`)
|
|
||||||
this.capeUrl = `${this.baseUrl}/textures/${cape.hash}`
|
|
||||||
} else {
|
|
||||||
this.capeUrl = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async changeName(player) {
|
|
||||||
let value
|
|
||||||
try {
|
|
||||||
({ value } = await showModal({
|
|
||||||
mode: 'prompt',
|
|
||||||
text: this.$t('user.changePlayerName'),
|
|
||||||
input: player.name,
|
|
||||||
validator: truthy(this.$t('user.emptyPlayerName')),
|
|
||||||
}))
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { code, message } = await this.$http.post(
|
|
||||||
`/user/player/rename/${player.pid}`,
|
|
||||||
{ name: value },
|
|
||||||
)
|
|
||||||
if (code === 0) {
|
|
||||||
toast.success(message)
|
|
||||||
player.name = value
|
|
||||||
} else {
|
|
||||||
toast.error(message)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async clearTexture() {
|
|
||||||
if (Object.values(this.clear).every(value => !value)) {
|
|
||||||
toast.error(this.$t('user.noClearChoice'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { code, message } = await this.$http.post(
|
|
||||||
`/user/player/texture/clear/${this.selected}`,
|
|
||||||
this.clear,
|
|
||||||
)
|
|
||||||
if (code === 0) {
|
|
||||||
toast.success(message)
|
|
||||||
const player = this.players.find(({ pid }) => pid === this.selected)
|
|
||||||
Object.keys(this.clear)
|
|
||||||
.filter(type => this.clear[type])
|
|
||||||
.forEach(type => (player[`tid_${type}`] = 0))
|
|
||||||
} else {
|
|
||||||
toast.error(message)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async deletePlayer(player, index) {
|
|
||||||
try {
|
|
||||||
await showModal({
|
|
||||||
title: this.$t('user.deletePlayer'),
|
|
||||||
text: this.$t('user.deletePlayerNotice'),
|
|
||||||
okButtonType: 'danger',
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { code, message } = await this.$http.post(`/user/player/delete/${player.pid}`)
|
|
||||||
if (code === 0) {
|
|
||||||
this.$delete(this.players, index)
|
|
||||||
toast.success(message)
|
|
||||||
} else {
|
|
||||||
toast.error(message)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.player
|
|
||||||
cursor pointer
|
|
||||||
border-bottom 1px solid #f4f4f4
|
|
||||||
|
|
||||||
.pid, .player-name
|
|
||||||
padding-top 13px
|
|
||||||
|
|
||||||
.player:last-child
|
|
||||||
border-bottom none
|
|
||||||
|
|
||||||
.player-selected
|
|
||||||
background-color #f5f5f5
|
|
||||||
|
|
||||||
.skin2d
|
|
||||||
float right
|
|
||||||
max-height 64px
|
|
||||||
width 64px
|
|
||||||
font-size 16px
|
|
||||||
|
|
||||||
#preview-2d > p
|
|
||||||
height 64px
|
|
||||||
line-height 64px
|
|
||||||
</style>
|
|
||||||
86
resources/assets/src/views/user/Players/ModalAddPlayer.tsx
Normal file
86
resources/assets/src/views/user/Players/ModalAddPlayer.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import * as fetch from '@/scripts/net'
|
||||||
|
import { toast } from '@/scripts/notify'
|
||||||
|
import { Player } from '@/scripts/types'
|
||||||
|
import Modal from '@/components/Modal'
|
||||||
|
|
||||||
|
type Extra = {
|
||||||
|
score: number
|
||||||
|
cost: number
|
||||||
|
rule: string
|
||||||
|
length: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
show: boolean
|
||||||
|
onAdd(player: Player): void
|
||||||
|
onClose(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalAddPlayer: React.FC<Props> = props => {
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
|
||||||
|
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setName(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
const { code, message, data: player } = await fetch.post<
|
||||||
|
fetch.ResponseBody<Player>
|
||||||
|
>('/user/player/add', { name })
|
||||||
|
if (code === 0) {
|
||||||
|
toast.success(message)
|
||||||
|
props.onAdd(player)
|
||||||
|
} else {
|
||||||
|
toast.error(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setName('')
|
||||||
|
props.onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { score, cost, rule, length } = blessing.extra as Extra
|
||||||
|
const isScoreEnough = score >= cost
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show={props.show}
|
||||||
|
title={t('user.player.add-player')}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="new-player-name">
|
||||||
|
{t('general.player.player-name')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="new-player-name"
|
||||||
|
value={name}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="callout callout-info">
|
||||||
|
<ul className="m-0 p-0 pl-3">
|
||||||
|
<li>{rule}</li>
|
||||||
|
<li>{length}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`alert alert-${isScoreEnough ? 'success' : 'danger'}`}
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<i className={`icon fas fa-${isScoreEnough ? 'check' : 'times'}`}></i>
|
||||||
|
<span className="ml-1">
|
||||||
|
{t('user.cur-score')} {score}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalAddPlayer
|
||||||
62
resources/assets/src/views/user/Players/ModalReset.tsx
Normal file
62
resources/assets/src/views/user/Players/ModalReset.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import Modal from '@/components/Modal'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
show: boolean
|
||||||
|
onSubmit(skin: boolean, cape: boolean): Promise<void>
|
||||||
|
onClose(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalReset: React.FC<Props> = props => {
|
||||||
|
const [skin, setSkin] = useState(false)
|
||||||
|
const [cape, setCape] = useState(false)
|
||||||
|
|
||||||
|
const handleSkinChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSkin(event.target.checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCapeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setCape(event.target.checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
props.onSubmit(skin, cape)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setSkin(false)
|
||||||
|
setCape(false)
|
||||||
|
props.onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show={props.show}
|
||||||
|
title={t('user.chooseClearTexture')}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<label className="d-block">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mr-2"
|
||||||
|
checked={skin}
|
||||||
|
onChange={handleSkinChange}
|
||||||
|
/>
|
||||||
|
{t('general.skin')}
|
||||||
|
</label>
|
||||||
|
<label className="d-block">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mr-2"
|
||||||
|
checked={cape}
|
||||||
|
onChange={handleCapeChange}
|
||||||
|
/>
|
||||||
|
{t('general.cape')}
|
||||||
|
</label>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalReset
|
||||||
55
resources/assets/src/views/user/Players/Previewer.tsx
Normal file
55
resources/assets/src/views/user/Players/Previewer.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import ViewerSkeleton from '@/components/ViewerSkeleton'
|
||||||
|
import Viewer2d from './Viewer2d'
|
||||||
|
|
||||||
|
const Viewer3d = React.lazy(() => import('@/components/Viewer'))
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
skin: string
|
||||||
|
cape: string
|
||||||
|
isAlex: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement('div')
|
||||||
|
|
||||||
|
const Previewer: React.FC<Props> = props => {
|
||||||
|
const [is3d, setIs3d] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mount = document.querySelector('#previewer')!
|
||||||
|
mount.appendChild(container)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mount.removeChild(container)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const switchMode = () => setIs3d(is => !is)
|
||||||
|
|
||||||
|
const switcher = (
|
||||||
|
<button className="btn btn-default" onClick={switchMode}>
|
||||||
|
{is3d ? t('user.switch2dPreview') : t('user.switch3dPreview')}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
const { skin, cape, isAlex } = props
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
is3d ? (
|
||||||
|
<React.Suspense fallback={<ViewerSkeleton />}>
|
||||||
|
<Viewer3d skin={skin} cape={cape} isAlex={isAlex}>
|
||||||
|
{switcher}
|
||||||
|
</Viewer3d>
|
||||||
|
</React.Suspense>
|
||||||
|
) : (
|
||||||
|
<Viewer2d skin={skin} cape={cape}>
|
||||||
|
{switcher}
|
||||||
|
</Viewer2d>
|
||||||
|
),
|
||||||
|
container
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Previewer
|
||||||
7
resources/assets/src/views/user/Players/Row.scss
Normal file
7
resources/assets/src/views/user/Players/Row.scss
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
.row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background-color: #efefef;
|
||||||
|
}
|
||||||
51
resources/assets/src/views/user/Players/Row.tsx
Normal file
51
resources/assets/src/views/user/Players/Row.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import ButtonEdit from '@/components/ButtonEdit'
|
||||||
|
import { Player } from '@/scripts/types'
|
||||||
|
import styles from './Row.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
player: Player
|
||||||
|
selected: boolean
|
||||||
|
onClick: React.MouseEventHandler
|
||||||
|
onEditName(player: Player): Promise<void>
|
||||||
|
onReset(): void
|
||||||
|
onDelete(player: Player): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const Row: React.FC<Props> = props => {
|
||||||
|
const { player } = props
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
props.onEditName(player)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
props.onDelete(player)
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = [styles.row]
|
||||||
|
if (props.selected) {
|
||||||
|
classes.push(styles.selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr className={classes.join(' ')} onClick={props.onClick}>
|
||||||
|
<td>{player.pid}</td>
|
||||||
|
<td>
|
||||||
|
<span>{player.name}</span>
|
||||||
|
<ButtonEdit title={t('user.player.edit-pname')} onClick={handleEdit} />
|
||||||
|
</td>
|
||||||
|
<td className="d-flex">
|
||||||
|
<button className="btn btn-warning" onClick={props.onReset}>
|
||||||
|
{t('user.player.delete-texture')}
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-danger ml-2" onClick={handleDelete}>
|
||||||
|
{t('user.player.delete-player')}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Row
|
||||||
8
resources/assets/src/views/user/Players/Viewer2d.scss
Normal file
8
resources/assets/src/views/user/Players/Viewer2d.scss
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
.texture {
|
||||||
|
max-height: 64px;
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
47
resources/assets/src/views/user/Players/Viewer2d.tsx
Normal file
47
resources/assets/src/views/user/Players/Viewer2d.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import styles from './Viewer2d.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
skin: string
|
||||||
|
cape: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Viewer2d: React.FC<Props> = props => {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3 className="card-title">{t('general.texturePreview')}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className={`d-flex justify-content-between mb-5 ${styles.line}`}>
|
||||||
|
<span>{t('general.skin')}</span>
|
||||||
|
{props.skin ? (
|
||||||
|
<img
|
||||||
|
src={props.skin}
|
||||||
|
className={styles.texture}
|
||||||
|
alt={t('general.skin')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>{t('user.player.texture-empty')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={`d-flex justify-content-between mt-5 ${styles.line}`}>
|
||||||
|
<span>{t('general.cape')}</span>
|
||||||
|
{props.cape ? (
|
||||||
|
<img
|
||||||
|
src={props.cape}
|
||||||
|
className={styles.texture}
|
||||||
|
alt={t('general.cape')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>{t('user.player.texture-empty')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-footer">{props.children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Viewer2d
|
||||||
221
resources/assets/src/views/user/Players/index.tsx
Normal file
221
resources/assets/src/views/user/Players/index.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { hot } from 'react-hot-loader/root'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import * as fetch from '@/scripts/net'
|
||||||
|
import { showModal, toast } from '@/scripts/notify'
|
||||||
|
import useTexture from '@/scripts/hooks/useTexture'
|
||||||
|
import { Player } from '@/scripts/types'
|
||||||
|
import Loading from '@/components/Loading'
|
||||||
|
import Row from './Row'
|
||||||
|
import Previewer from './Previewer'
|
||||||
|
import ModalAddPlayer from './ModalAddPlayer'
|
||||||
|
import ModalReset from './ModalReset'
|
||||||
|
|
||||||
|
const Players: React.FC = () => {
|
||||||
|
const [players, setPlayers] = useState<Player[]>([])
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [selected, setSelected] = useState(0)
|
||||||
|
const [skin, setSkin] = useTexture()
|
||||||
|
const [cape, setCape] = useTexture()
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [showModalAddPlayer, setShowModalAddPlayer] = useState(false)
|
||||||
|
const [showModalReset, setShowModalReset] = useState(false)
|
||||||
|
|
||||||
|
const selectPlayer = (player: Player) => {
|
||||||
|
setSelected(player.pid)
|
||||||
|
setSkin(player.tid_skin)
|
||||||
|
setCape(player.tid_cape)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getPlayers = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
const { data } = await fetch.get<fetch.ResponseBody<Player[]>>(
|
||||||
|
'/user/player/list',
|
||||||
|
)
|
||||||
|
setPlayers(data)
|
||||||
|
if (data.length === 1) {
|
||||||
|
selectPlayer(data[0])
|
||||||
|
}
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
getPlayers()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = (player: Player) => {
|
||||||
|
setPlayers(players => [...players, player])
|
||||||
|
}
|
||||||
|
|
||||||
|
const editName = async (player: Player, index: number) => {
|
||||||
|
let name: string
|
||||||
|
try {
|
||||||
|
const { value } = await showModal({
|
||||||
|
mode: 'prompt',
|
||||||
|
text: t('user.changePlayerName'),
|
||||||
|
input: player.name,
|
||||||
|
validator: (value: string) => {
|
||||||
|
if (!value) {
|
||||||
|
return t('user.emptyPlayerName')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
name = value
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||||
|
`/user/player/rename/${player.pid}`,
|
||||||
|
{ name },
|
||||||
|
)
|
||||||
|
if (code === 0) {
|
||||||
|
toast.success(message)
|
||||||
|
setPlayers(players => {
|
||||||
|
players[index] = { ...player, name }
|
||||||
|
return players.slice()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.error(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetTexture = async (skin: boolean, cape: boolean) => {
|
||||||
|
if (!skin && !cape) {
|
||||||
|
toast.warning(t('user.noClearChoice'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code, message } = await fetch.post<fetch.ResponseBody>(
|
||||||
|
`/user/player/texture/clear/${selected}`,
|
||||||
|
{ type: [skin && 'skin', cape && 'cape'].filter(Boolean) },
|
||||||
|
)
|
||||||
|
if (code === 0) {
|
||||||
|
toast.success(message)
|
||||||
|
if (skin) {
|
||||||
|
setSkin(0)
|
||||||
|
}
|
||||||
|
if (cape) {
|
||||||
|
setCape(0)
|
||||||
|
}
|
||||||
|
setPlayers(players => {
|
||||||
|
const index = players.findIndex(player => player.pid === selected)
|
||||||
|
const player = Object.assign({}, players[index])
|
||||||
|
if (skin) {
|
||||||
|
player.tid_skin = 0
|
||||||
|
}
|
||||||
|
if (cape) {
|
||||||
|
player.tid_cape = 0
|
||||||
|
}
|
||||||
|
players[index] = player
|
||||||
|
return players.slice()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.error(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletePlayer = async (player: Player) => {
|
||||||
|
try {
|
||||||
|
await showModal({
|
||||||
|
title: t('user.deletePlayer'),
|
||||||
|
text: t('user.deletePlayerNotice'),
|
||||||
|
okButtonType: 'danger',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { code, message } = await fetch.post(
|
||||||
|
`/user/player/delete/${player.pid}`,
|
||||||
|
)
|
||||||
|
if (code === 0) {
|
||||||
|
toast.success(message)
|
||||||
|
const { pid } = player
|
||||||
|
setPlayers(players => players.filter(player => player.pid !== pid))
|
||||||
|
} else {
|
||||||
|
toast.error(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openModalAddPlayer = () => setShowModalAddPlayer(true)
|
||||||
|
const closeModalAddPlayer = () => setShowModalAddPlayer(false)
|
||||||
|
|
||||||
|
const openModalReset = () => setShowModalReset(true)
|
||||||
|
const closeModalReset = () => setShowModalReset(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
placeholder={t('user.typeToSearch')}
|
||||||
|
onChange={handleSearch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="card-body p-0 table-responsive">
|
||||||
|
<table className="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: '12%' }}>PID</th>
|
||||||
|
<th>{t('general.player.player-name')}</th>
|
||||||
|
<th style={{ width: '50%' }}>{t('user.player.operation')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{players.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td className="text-center" colSpan={3}>
|
||||||
|
{isLoading ? <Loading /> : 'Nothing here.'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
players
|
||||||
|
.filter(({ name }) => name.includes(search))
|
||||||
|
.map((player, i) => (
|
||||||
|
<Row
|
||||||
|
key={player.pid}
|
||||||
|
player={player}
|
||||||
|
selected={selected === player.pid}
|
||||||
|
onClick={() => selectPlayer(player)}
|
||||||
|
onEditName={() => editName(player, i)}
|
||||||
|
onReset={openModalReset}
|
||||||
|
onDelete={deletePlayer}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="card-footer">
|
||||||
|
<button className="btn btn-primary" onClick={openModalAddPlayer}>
|
||||||
|
<i className="fas fa-plus mr-1"></i>
|
||||||
|
<span>{t('user.player.add-player')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Previewer
|
||||||
|
skin={skin.url}
|
||||||
|
cape={cape.url}
|
||||||
|
isAlex={skin.type === 'alex'}
|
||||||
|
/>
|
||||||
|
<ModalAddPlayer
|
||||||
|
show={showModalAddPlayer}
|
||||||
|
onAdd={handleAdd}
|
||||||
|
onClose={closeModalAddPlayer}
|
||||||
|
/>
|
||||||
|
<ModalReset
|
||||||
|
show={showModalReset}
|
||||||
|
onSubmit={resetTexture}
|
||||||
|
onClose={closeModalReset}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hot(Players)
|
||||||
516
resources/assets/tests/views/user/Players.test.tsx
Normal file
516
resources/assets/tests/views/user/Players.test.tsx
Normal file
|
|
@ -0,0 +1,516 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { render, fireEvent, wait } from '@testing-library/react'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import * as fetch from '@/scripts/net'
|
||||||
|
import { Player } from '@/scripts/types'
|
||||||
|
import Players from '@/views/user/Players'
|
||||||
|
|
||||||
|
jest.mock('@/scripts/net')
|
||||||
|
|
||||||
|
const fixture: Readonly<Player> = Object.freeze<Player>({
|
||||||
|
pid: 1,
|
||||||
|
name: 'kumiko',
|
||||||
|
tid_skin: 1,
|
||||||
|
tid_cape: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.id = 'previewer'
|
||||||
|
document.body.appendChild(container)
|
||||||
|
|
||||||
|
blessing.extra = {
|
||||||
|
rule: 'please follow the rule',
|
||||||
|
length: 'greater than 3',
|
||||||
|
score: 1000,
|
||||||
|
cost: 50,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
document.querySelector('#previewer')!.remove()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('loading indicator', () => {
|
||||||
|
fetch.get.mockResolvedValue({ data: [] })
|
||||||
|
const { queryByTitle } = render(<Players />)
|
||||||
|
expect(queryByTitle('Loading...')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('search players', async () => {
|
||||||
|
const fixture2: Player = { pid: 2, name: 'reina', tid_skin: 3, tid_cape: 4 }
|
||||||
|
fetch.get.mockResolvedValue({ data: [fixture, fixture2] })
|
||||||
|
|
||||||
|
const { getByPlaceholderText, queryByText } = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.input(getByPlaceholderText(t('user.typeToSearch')), {
|
||||||
|
target: { value: 'k' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(queryByText(fixture.name)).toBeInTheDocument()
|
||||||
|
expect(queryByText(fixture2.name)).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('select player automatically', () => {
|
||||||
|
it('only one player', async () => {
|
||||||
|
fetch.get.mockResolvedValue({ data: [fixture] })
|
||||||
|
render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toBeCalledWith(`/skinlib/info/${fixture.tid_skin}`)
|
||||||
|
expect(fetch.get).toBeCalledWith(`/skinlib/info/${fixture.tid_cape}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('more players', async () => {
|
||||||
|
const fixture2: Player = { pid: 2, name: 'reina', tid_skin: 3, tid_cape: 4 }
|
||||||
|
fetch.get.mockResolvedValue({ data: [fixture, fixture2] })
|
||||||
|
render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).not.toBeCalledWith(`/skinlib/info/${fixture.tid_skin}`)
|
||||||
|
expect(fetch.get).not.toBeCalledWith(`/skinlib/info/${fixture.tid_cape}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('2d preview', () => {
|
||||||
|
it('skin and cape', async () => {
|
||||||
|
fetch.get
|
||||||
|
.mockResolvedValueOnce({ data: [fixture] })
|
||||||
|
.mockResolvedValueOnce({ data: { hash: 'a', type: 'steve' } })
|
||||||
|
.mockResolvedValueOnce({ data: { hash: 'b', type: 'cape' } })
|
||||||
|
|
||||||
|
const { getByAltText, getByText } = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.switch2dPreview')))
|
||||||
|
|
||||||
|
expect(getByAltText(t('general.skin'))).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
`${blessing.base_url}/textures/a`,
|
||||||
|
)
|
||||||
|
expect(getByAltText(t('general.cape'))).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
`${blessing.base_url}/textures/b`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skin only', async () => {
|
||||||
|
fetch.get
|
||||||
|
.mockResolvedValueOnce({ data: [{ ...fixture, tid_cape: 0 }] })
|
||||||
|
.mockResolvedValueOnce({ data: { hash: 'a', type: 'steve' } })
|
||||||
|
|
||||||
|
const { getByAltText, queryByAltText, getByText, queryByText } = render(
|
||||||
|
<Players />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.switch2dPreview')))
|
||||||
|
|
||||||
|
expect(getByAltText(t('general.skin'))).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
`${blessing.base_url}/textures/a`,
|
||||||
|
)
|
||||||
|
expect(queryByAltText(t('general.cape'))).not.toBeInTheDocument()
|
||||||
|
expect(queryByText(t('user.player.texture-empty'))).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cape only', async () => {
|
||||||
|
fetch.get
|
||||||
|
.mockResolvedValueOnce({ data: [{ ...fixture, tid_skin: 0 }] })
|
||||||
|
.mockResolvedValueOnce({ data: { hash: 'a', type: 'cape' } })
|
||||||
|
|
||||||
|
const { getByAltText, queryByAltText, getByText, queryByText } = render(
|
||||||
|
<Players />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.switch2dPreview')))
|
||||||
|
|
||||||
|
expect(getByAltText(t('general.cape'))).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
`${blessing.base_url}/textures/a`,
|
||||||
|
)
|
||||||
|
expect(queryByAltText(t('general.skin'))).not.toBeInTheDocument()
|
||||||
|
expect(queryByText(t('user.player.texture-empty'))).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('create player', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.get.mockResolvedValue({ data: [] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('alert if score is enough', async () => {
|
||||||
|
const { getByRole, getByText, queryByText } = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.add-player')))
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByText(`${t('user.cur-score')} ${blessing.extra.score}`),
|
||||||
|
).toBeInTheDocument()
|
||||||
|
expect(getByRole('alert')).toHaveClass('alert-success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('alert if lack of score', async () => {
|
||||||
|
blessing.extra.score = 0
|
||||||
|
const { getByRole, getByText, queryByText } = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.add-player')))
|
||||||
|
|
||||||
|
expect(
|
||||||
|
queryByText(`${t('user.cur-score')} ${blessing.extra.score}`),
|
||||||
|
).toBeInTheDocument()
|
||||||
|
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('succeeded', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 0, message: 'success', data: fixture })
|
||||||
|
|
||||||
|
const { getByText, getByLabelText, getByRole, queryByText } = render(
|
||||||
|
<Players />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.add-player')))
|
||||||
|
fireEvent.input(getByLabelText(t('general.player.player-name')), {
|
||||||
|
target: { value: fixture.name },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalledWith('/user/player/add', {
|
||||||
|
name: fixture.name,
|
||||||
|
})
|
||||||
|
expect(queryByText('success')).toBeInTheDocument()
|
||||||
|
expect(getByRole('status')).toHaveClass('alert-success')
|
||||||
|
expect(queryByText(fixture.pid.toString())).toBeInTheDocument()
|
||||||
|
expect(queryByText(fixture.name)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||||
|
|
||||||
|
const { getByText, getByLabelText, getByRole, queryByText } = render(
|
||||||
|
<Players />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.add-player')))
|
||||||
|
fireEvent.input(getByLabelText(t('general.player.player-name')), {
|
||||||
|
target: { value: fixture.name },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalledWith('/user/player/add', {
|
||||||
|
name: fixture.name,
|
||||||
|
})
|
||||||
|
expect(queryByText('failed')).toBeInTheDocument()
|
||||||
|
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||||
|
expect(queryByText(fixture.name)).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancelled', async () => {
|
||||||
|
const { getByText, getByLabelText, queryByText } = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.add-player')))
|
||||||
|
fireEvent.input(getByLabelText(t('general.player.player-name')), {
|
||||||
|
target: { value: fixture.name },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(t('general.cancel')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
expect(queryByText(fixture.name)).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clear form on close', async () => {
|
||||||
|
const { getByText, getByLabelText } = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.add-player')))
|
||||||
|
fireEvent.input(getByLabelText(t('general.player.player-name')), {
|
||||||
|
target: { value: fixture.name },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(t('general.cancel')))
|
||||||
|
fireEvent.click(getByText(t('user.player.add-player')))
|
||||||
|
|
||||||
|
expect(getByLabelText(t('general.player.player-name'))).toHaveValue('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('edit player name', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.get.mockResolvedValue({ data: [fixture] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('succeeded', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
|
||||||
|
|
||||||
|
const {
|
||||||
|
getByText,
|
||||||
|
getByTitle,
|
||||||
|
getByDisplayValue,
|
||||||
|
getByRole,
|
||||||
|
queryByText,
|
||||||
|
} = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByTitle(t('user.player.edit-pname')))
|
||||||
|
fireEvent.input(getByDisplayValue(fixture.name), {
|
||||||
|
target: { value: 'reina' },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalledWith(`/user/player/rename/${fixture.pid}`, {
|
||||||
|
name: 'reina',
|
||||||
|
})
|
||||||
|
expect(queryByText('success')).toBeInTheDocument()
|
||||||
|
expect(getByRole('status')).toHaveClass('alert-success')
|
||||||
|
expect(queryByText('reina')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('empty name', async () => {
|
||||||
|
const { getByText, getByTitle, getByDisplayValue, queryByText } = render(
|
||||||
|
<Players />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByTitle(t('user.player.edit-pname')))
|
||||||
|
fireEvent.input(getByDisplayValue(fixture.name), {
|
||||||
|
target: { value: '' },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
expect(queryByText(t('user.emptyPlayerName'))).toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('general.cancel')))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||||
|
|
||||||
|
const {
|
||||||
|
getByText,
|
||||||
|
getByTitle,
|
||||||
|
getByDisplayValue,
|
||||||
|
getByRole,
|
||||||
|
queryByText,
|
||||||
|
} = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByTitle(t('user.player.edit-pname')))
|
||||||
|
fireEvent.input(getByDisplayValue(fixture.name), {
|
||||||
|
target: { value: 'reina' },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalledWith(`/user/player/rename/${fixture.pid}`, {
|
||||||
|
name: 'reina',
|
||||||
|
})
|
||||||
|
expect(queryByText('failed')).toBeInTheDocument()
|
||||||
|
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||||
|
expect(queryByText(fixture.name)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancelled', async () => {
|
||||||
|
const { getByText, getByTitle, getByDisplayValue, queryByText } = render(
|
||||||
|
<Players />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByTitle(t('user.player.edit-pname')))
|
||||||
|
fireEvent.input(getByDisplayValue(fixture.name), {
|
||||||
|
target: { value: 'reina' },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(t('general.cancel')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
expect(queryByText(fixture.name)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('reset texture', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.get.mockResolvedValue({ data: [fixture] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clear skin and cape', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
|
||||||
|
|
||||||
|
const { getByText, getByRole, getByLabelText, queryByText } = render(
|
||||||
|
<Players />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.delete-texture')))
|
||||||
|
fireEvent.click(getByLabelText(t('general.skin')))
|
||||||
|
fireEvent.click(getByLabelText(t('general.cape')))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalledWith(
|
||||||
|
`/user/player/texture/clear/${fixture.pid}`,
|
||||||
|
{
|
||||||
|
type: ['skin', 'cape'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(queryByText('success')).toBeInTheDocument()
|
||||||
|
expect(getByRole('status')).toHaveClass('alert-success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clear skin', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
|
||||||
|
|
||||||
|
const { getByText, getByRole, getByLabelText, queryByText } = render(
|
||||||
|
<Players />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.delete-texture')))
|
||||||
|
fireEvent.click(getByLabelText(t('general.skin')))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalledWith(
|
||||||
|
`/user/player/texture/clear/${fixture.pid}`,
|
||||||
|
{
|
||||||
|
type: ['skin'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(queryByText('success')).toBeInTheDocument()
|
||||||
|
expect(getByRole('status')).toHaveClass('alert-success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clear cape', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
|
||||||
|
|
||||||
|
const { getByText, getByRole, getByLabelText, queryByText } = render(
|
||||||
|
<Players />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.delete-texture')))
|
||||||
|
fireEvent.click(getByLabelText(t('general.cape')))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalledWith(
|
||||||
|
`/user/player/texture/clear/${fixture.pid}`,
|
||||||
|
{
|
||||||
|
type: ['cape'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(queryByText('success')).toBeInTheDocument()
|
||||||
|
expect(getByRole('status')).toHaveClass('alert-success')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('select nothing', async () => {
|
||||||
|
const { getByText, getByRole, queryByText } = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.delete-texture')))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
expect(queryByText(t('user.noClearChoice'))).toBeInTheDocument()
|
||||||
|
expect(getByRole('alert')).toHaveClass('alert-warning')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||||
|
|
||||||
|
const { getByText, getByRole, getByLabelText, queryByText } = render(
|
||||||
|
<Players />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.delete-texture')))
|
||||||
|
fireEvent.click(getByLabelText(t('general.skin')))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalledWith(
|
||||||
|
`/user/player/texture/clear/${fixture.pid}`,
|
||||||
|
{
|
||||||
|
type: ['skin'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(queryByText('failed')).toBeInTheDocument()
|
||||||
|
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancelled', async () => {
|
||||||
|
const { getByText, getByRole, getByLabelText, queryByText } = render(
|
||||||
|
<Players />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.delete-texture')))
|
||||||
|
fireEvent.click(getByLabelText(t('general.skin')))
|
||||||
|
fireEvent.click(getByText(t('general.cancel')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('delete player', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.get.mockResolvedValue({ data: [fixture] })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('succeeded', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 0, message: 'success' })
|
||||||
|
|
||||||
|
const { getByText, getByRole, queryByText } = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.delete-player')))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalledWith(`/user/player/delete/${fixture.pid}`)
|
||||||
|
expect(getByText('success')).toBeInTheDocument()
|
||||||
|
expect(getByRole('status')).toHaveClass('alert-success')
|
||||||
|
expect(queryByText(fixture.name)).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||||
|
|
||||||
|
const { getByText, getByRole, queryByText } = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.delete-player')))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalledWith(`/user/player/delete/${fixture.pid}`)
|
||||||
|
expect(getByText('failed')).toBeInTheDocument()
|
||||||
|
expect(getByRole('alert')).toHaveClass('alert-danger')
|
||||||
|
expect(queryByText(fixture.name)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancelled', async () => {
|
||||||
|
const { getByText, getByRole, queryByText } = render(<Players />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('user.player.delete-player')))
|
||||||
|
fireEvent.click(getByText(t('general.cancel')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
expect(queryByText(fixture.name)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user