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',
|
||||
['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 [
|
||||
{
|
||||
path: '/',
|
||||
module: [
|
||||
() => import('../styles/home.styl'),
|
||||
() => import('./home-page'),
|
||||
],
|
||||
module: [() => import('../styles/home.styl'), () => import('./home-page')],
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
|
|
@ -20,7 +17,7 @@ export default [
|
|||
<div className="card-body"></div>
|
||||
<div className="card-footer"> </div>
|
||||
</div>
|
||||
)
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'user/closet',
|
||||
|
|
@ -29,8 +26,14 @@ export default [
|
|||
},
|
||||
{
|
||||
path: 'user/player',
|
||||
component: () => import('../views/user/Players.vue'),
|
||||
el: virtual,
|
||||
react: () => import('../views/user/Players'),
|
||||
el: '#players-list',
|
||||
frame: () => (
|
||||
<div className="card">
|
||||
<div className="card-header"> </div>
|
||||
<div className="card-body p-0"></div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'user/player/bind',
|
||||
|
|
@ -39,9 +42,7 @@ export default [
|
|||
},
|
||||
{
|
||||
path: 'user/profile',
|
||||
module: [
|
||||
() => import('../views/user/profile/index'),
|
||||
],
|
||||
module: [() => import('../views/user/profile/index')],
|
||||
},
|
||||
{
|
||||
path: 'user/oauth/manage',
|
||||
|
|
@ -50,9 +51,7 @@ export default [
|
|||
},
|
||||
{
|
||||
path: 'admin',
|
||||
module: [
|
||||
() => import('../views/admin/Dashboard'),
|
||||
],
|
||||
module: [() => import('../views/admin/Dashboard')],
|
||||
},
|
||||
{
|
||||
path: 'admin/users',
|
||||
|
|
@ -71,9 +70,7 @@ export default [
|
|||
},
|
||||
{
|
||||
path: 'admin/customize',
|
||||
module: [
|
||||
() => import('../views/admin/Customization'),
|
||||
],
|
||||
module: [() => import('../views/admin/Customization')],
|
||||
},
|
||||
{
|
||||
path: 'admin/i18n',
|
||||
|
|
@ -92,9 +89,7 @@ export default [
|
|||
},
|
||||
{
|
||||
path: 'admin/update',
|
||||
module: [
|
||||
() => import('../views/admin/Update'),
|
||||
],
|
||||
module: [() => import('../views/admin/Update')],
|
||||
},
|
||||
{
|
||||
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