Add grid for closet page
This commit is contained in:
parent
6ead313999
commit
a3e74065f9
|
|
@ -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')),
|
||||
|
|
|
|||
28
resources/assets/src/components/Portal.ts
Normal file
28
resources/assets/src/components/Portal.ts
Normal 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])
|
||||
},
|
||||
})
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
50
resources/assets/tests/components/Portal.test.ts
Normal file
50
resources/assets/tests/components/Portal.test.ts
Normal 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')
|
||||
})
|
||||
|
|
@ -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: {} })
|
||||
|
|
|
|||
1
resources/views/shared/previewer.twig
Normal file
1
resources/views/shared/previewer.twig
Normal file
|
|
@ -0,0 +1 @@
|
|||
<div id="previewer"></div>
|
||||
|
|
@ -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', {
|
||||
|
|
|
|||
1
resources/views/user/widgets/closet/list.twig
Normal file
1
resources/views/user/widgets/closet/list.twig
Normal file
|
|
@ -0,0 +1 @@
|
|||
<div id="closet-list"></div>
|
||||
1
resources/views/user/widgets/email-verification.twig
Normal file
1
resources/views/user/widgets/email-verification.twig
Normal file
|
|
@ -0,0 +1 @@
|
|||
<div id="email-verification"></div>
|
||||
Loading…
Reference in New Issue
Block a user