Add grid for closet page

This commit is contained in:
Pig Fang 2019-12-14 14:30:38 +08:00
parent 6ead313999
commit a3e74065f9
10 changed files with 282 additions and 164 deletions

View File

@ -4,14 +4,32 @@ namespace App\Http\Controllers;
use App\Models\Texture;
use App\Models\User;
use App\Services\Filter;
use Auth;
use Illuminate\Http\Request;
class ClosetController extends Controller
{
public function index()
public function index(Filter $filter)
{
$grid = [
'layout' => [
['md-8', 'md-4'],
],
'widgets' => [
[
[
'user.widgets.email-verification',
'user.widgets.closet.list',
],
['shared.previewer'],
],
],
];
$grid = $filter->apply('grid:user.closet', $grid);
return view('user.closet')
->with('grid', $grid)
->with('extra', [
'unverified' => option('require_verification') && !auth()->user()->verified,
'rule' => trans('user.player.player-name-rule.'.option('player_name_rule')),

View File

@ -0,0 +1,28 @@
import Vue from 'vue'
export default Vue.extend({
name: 'Portal',
props: {
selector: {
required: true,
type: String,
},
tag: {
type: String,
default: 'div',
},
},
mounted() {
const container = document.querySelector(this.selector)
if (container) {
if (container.firstChild) {
container.replaceChild(this.$el, container.firstChild)
} else {
container.appendChild(this.$el)
}
}
},
render(h) {
return h(this.tag, [this.$slots.default])
},
})

View File

@ -1,3 +1,5 @@
const offline = document.createElement('div')
export default [
{
path: '/',
@ -14,7 +16,7 @@ export default [
{
path: 'user/closet',
component: () => import('../views/user/Closet.vue'),
el: '.content > .container-fluid',
el: offline,
},
{
path: 'user/player',

View File

@ -1,178 +1,181 @@
<template>
<div class="container-fluid">
<email-verification />
<div class="row">
<div class="col-md-8">
<div class="card card-primary card-tabs">
<div class="card-header p-0 pt-1 pl-1">
<div class="d-flex justify-content-between">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a
class="nav-link"
:class="{ active: category === 'skin' }"
href="#"
data-toggle="pill"
role="tab"
@click="switchCategory"
>
{{ $t('general.skin') }}
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
:class="{ active: category === 'cape' }"
href="#"
@click="switchCategory"
>
{{ $t('general.cape') }}
</a>
</li>
<li class="nav-item d-none d-md-block">
<a
v-t="'user.closet.upload'"
:href="`${baseUrl}/skinlib/upload`"
class="nav-link"
/>
</li>
</ul>
<div class="mr-3 my-2 my-lg-0">
<input
v-model="query"
class="form-control mr-sm-2"
type="search"
aria-label="Search"
:placeholder="$t('user.typeToSearch')"
@input="search"
<div>
<portal selector="#email-verification"><email-verification /></portal>
<portal selector="#closet-list">
<div class="card card-primary card-tabs">
<div class="card-header p-0 pt-1 pl-1">
<div class="d-flex justify-content-between">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a
class="nav-link"
:class="{ active: category === 'skin' }"
href="#"
data-toggle="pill"
role="tab"
@click="switchCategory"
>
</div>
</div>
</div>
<div class="card-body">
<div
v-if="category === 'skin'"
id="skin-category"
:class="{ active: category === 'skin' }"
>
<div v-if="skinItems.length === 0" class="text-center p-3">
<div v-if="query !== ''" v-t="'general.noResult'" />
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-else v-html="$t('user.emptyClosetMsg', { url: linkToSkin })" />
</div>
<div v-else class="d-flex flex-wrap">
<closet-item
v-for="(item, index) in skinItems"
:key="item.tid"
:tid="item.tid"
:name="item.name"
:type="item.type"
:selected="selectedSkin === item.tid"
@select="selectTexture(item.tid)"
@item-removed="removeSkinItem(index)"
{{ $t('general.skin') }}
</a>
</li>
<li class="nav-item">
<a
class="nav-link"
:class="{ active: category === 'cape' }"
href="#"
@click="switchCategory"
>
{{ $t('general.cape') }}
</a>
</li>
<li class="nav-item d-none d-md-block">
<a
v-t="'user.closet.upload'"
:href="`${baseUrl}/skinlib/upload`"
class="nav-link"
/>
</div>
</li>
</ul>
<div class="mr-3 my-2 my-lg-0">
<input
v-model="query"
class="form-control mr-sm-2"
type="search"
aria-label="Search"
:placeholder="$t('user.typeToSearch')"
@input="search"
>
</div>
<div
v-else
id="cape-category"
:class="{ active: category === 'cape' }"
>
<div v-if="capeItems.length === 0" class="text-center p-3">
<div v-if="query !== ''" v-t="'general.noResult'" />
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-else v-html="$t('user.emptyClosetMsg', { url: linkToCape })" />
</div>
<div v-else>
<closet-item
v-for="(item, index) in capeItems"
:key="item.tid"
:tid="item.tid"
:name="item.name"
:type="item.type"
:selected="selectedCape === item.tid"
@select="selectTexture(item.tid)"
@item-removed="removeCapeItem(index)"
/>
</div>
</div>
</div>
<div class="card-footer">
<paginate
v-if="category === 'skin'"
v-model="skinCurrentPage"
:page-count="skinTotalPages"
class="float-right"
container-class="pagination pagination-sm no-margin"
page-class="page-item"
page-link-class="page-link"
prev-class="page-item"
prev-link-class="page-link"
next-class="page-item"
next-link-class="page-link"
first-button-text="«"
prev-text=""
next-text=""
last-button-text="»"
:click-handler="pageChanged"
:first-last-button="true"
/>
<paginate
v-else
v-model="capeCurrentPages"
:page-count="capeTotalPages"
class="float-right"
container-class="pagination pagination-sm no-margin"
page-class="page-item"
page-link-class="page-link"
prev-class="page-item"
prev-link-class="page-link"
next-class="page-item"
next-link-class="page-link"
first-button-text="«"
prev-text=""
next-text=""
last-button-text="»"
:click-handler="pageChanged"
:first-last-button="true"
/>
</div>
</div>
</div>
<div class="col-md-4">
<previewer
closet-mode
:skin="skinUrl"
:cape="capeUrl"
:model="model"
>
<template #footer>
<div class="d-flex justify-content-between">
<button
class="btn btn-primary"
data-toggle="modal"
data-target="#modal-use-as"
@click="fetchPlayersList"
>
{{ $t('user.useAs') }}
</button>
<button class="btn btn-default" data-test="resetSelected" @click="resetSelected">
{{ $t('user.resetSelected') }}
</button>
<div class="card-body">
<div
v-if="category === 'skin'"
id="skin-category"
:class="{ active: category === 'skin' }"
>
<div v-if="skinItems.length === 0" class="text-center p-3">
<div v-if="query !== ''" v-t="'general.noResult'" />
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-else v-html="$t('user.emptyClosetMsg', { url: linkToSkin })" />
</div>
</template>
</previewer>
<div v-else class="d-flex flex-wrap">
<closet-item
v-for="(item, index) in skinItems"
:key="item.tid"
:tid="item.tid"
:name="item.name"
:type="item.type"
:selected="selectedSkin === item.tid"
@select="selectTexture(item.tid)"
@item-removed="removeSkinItem(index)"
/>
</div>
</div>
<div
v-else
id="cape-category"
:class="{ active: category === 'cape' }"
>
<div v-if="capeItems.length === 0" class="text-center p-3">
<div v-if="query !== ''" v-t="'general.noResult'" />
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-else v-html="$t('user.emptyClosetMsg', { url: linkToCape })" />
</div>
<div v-else>
<closet-item
v-for="(item, index) in capeItems"
:key="item.tid"
:tid="item.tid"
:name="item.name"
:type="item.type"
:selected="selectedCape === item.tid"
@select="selectTexture(item.tid)"
@item-removed="removeCapeItem(index)"
/>
</div>
</div>
</div>
<div class="card-footer">
<paginate
v-if="category === 'skin'"
v-model="skinCurrentPage"
:page-count="skinTotalPages"
class="float-right"
container-class="pagination pagination-sm no-margin"
page-class="page-item"
page-link-class="page-link"
prev-class="page-item"
prev-link-class="page-link"
next-class="page-item"
next-link-class="page-link"
first-button-text="«"
prev-text=""
next-text=""
last-button-text="»"
:click-handler="pageChanged"
:first-last-button="true"
/>
<paginate
v-else
v-model="capeCurrentPages"
:page-count="capeTotalPages"
class="float-right"
container-class="pagination pagination-sm no-margin"
page-class="page-item"
page-link-class="page-link"
prev-class="page-item"
prev-link-class="page-link"
next-class="page-item"
next-link-class="page-link"
first-button-text="«"
prev-text=""
next-text=""
last-button-text="»"
:click-handler="pageChanged"
:first-last-button="true"
/>
</div>
</div>
</div>
<apply-to-player-dialog ref="useAs" :skin="selectedSkin" :cape="selectedCape" />
<add-player-dialog @add="fetchPlayersList" />
</portal>
<portal selector="#previewer">
<previewer
closet-mode
:skin="skinUrl"
:cape="capeUrl"
:model="model"
>
<template #footer>
<div class="d-flex justify-content-between">
<button
class="btn btn-primary"
data-toggle="modal"
data-target="#modal-use-as"
@click="fetchPlayersList"
>
{{ $t('user.useAs') }}
</button>
<button class="btn btn-default" data-test="resetSelected" @click="resetSelected">
{{ $t('user.resetSelected') }}
</button>
</div>
</template>
</previewer>
</portal>
<portal selector="#modals">
<apply-to-player-dialog ref="useAs" :skin="selectedSkin" :cape="selectedCape" />
<add-player-dialog @add="fetchPlayersList" />
</portal>
</div>
</template>
<script>
import Paginate from 'vuejs-paginate'
import { debounce, queryString } from '../../scripts/utils'
import Portal from '../../components/Portal'
import ClosetItem from '../../components/ClosetItem.vue'
import EmailVerification from '../../components/EmailVerification.vue'
import AddPlayerDialog from '../../components/AddPlayerDialog.vue'
@ -182,6 +185,7 @@ import emitMounted from '../../components/mixins/emitMounted'
export default {
name: 'Closet',
components: {
Portal,
Paginate,
ClosetItem,
Previewer: () => import('../../components/Previewer.vue'),

View File

@ -0,0 +1,50 @@
import { mount } from '@vue/test-utils'
import Portal from '@/components/Portal'
beforeEach(() => {
document.body.innerHTML = ''
})
test('render default slot', () => {
mount(Portal, {
propsData: {
selector: 'body',
},
slots: {
default: 'children',
},
})
expect(document.querySelectorAll('div')).toHaveLength(1)
expect(document.querySelector('div')!.textContent).toBe('children')
})
test('custom wrapper tag', () => {
mount(Portal, {
propsData: {
selector: 'body',
tag: 'span',
},
})
expect(document.querySelectorAll('span')).toHaveLength(1)
})
test('should pass if container does not exist', () => {
mount(Portal, {
propsData: {
selector: '#nope',
},
})
})
test('replace container content', () => {
document.body.innerHTML = '<div>before</div>'
mount(Portal, {
propsData: {
selector: 'body',
},
slots: {
default: 'after',
},
})
expect(document.querySelector('div')!.textContent).toBe('after')
})

View File

@ -5,7 +5,14 @@ import Closet from '@/views/user/Closet.vue'
import ClosetItem from '@/components/ClosetItem.vue'
import Previewer from '@/components/Previewer.vue'
window.blessing.extra = { unverified: false }
beforeEach(() => {
document.body.innerHTML = `
<div id="closet-list"></div>
<div id="previewer"></div>
`
window.blessing.extra = { unverified: false }
})
test('fetch closet data before mount', () => {
Vue.prototype.$http.get.mockResolvedValue({ data: {} })

View File

@ -0,0 +1 @@
<div id="previewer"></div>

View File

@ -2,6 +2,12 @@
{% block title %}{{ trans('general.my-closet') }}{% endblock %}
{% block content %}
{{ include('shared.grid') }}
<div id="modals"></div>
{% endblock %}
{% block before_foot %}
<script>
Object.defineProperty(blessing, 'extra', {

View File

@ -0,0 +1 @@
<div id="closet-list"></div>

View File

@ -0,0 +1 @@
<div id="email-verification"></div>