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\Texture;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\Filter;
|
||||||
use Auth;
|
use Auth;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class ClosetController extends Controller
|
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')
|
return view('user.closet')
|
||||||
|
->with('grid', $grid)
|
||||||
->with('extra', [
|
->with('extra', [
|
||||||
'unverified' => option('require_verification') && !auth()->user()->verified,
|
'unverified' => option('require_verification') && !auth()->user()->verified,
|
||||||
'rule' => trans('user.player.player-name-rule.'.option('player_name_rule')),
|
'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 [
|
export default [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|
@ -14,7 +16,7 @@ export default [
|
||||||
{
|
{
|
||||||
path: 'user/closet',
|
path: 'user/closet',
|
||||||
component: () => import('../views/user/Closet.vue'),
|
component: () => import('../views/user/Closet.vue'),
|
||||||
el: '.content > .container-fluid',
|
el: offline,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'user/player',
|
path: 'user/player',
|
||||||
|
|
|
||||||
|
|
@ -1,178 +1,181 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container-fluid">
|
<div>
|
||||||
<email-verification />
|
<portal selector="#email-verification"><email-verification /></portal>
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8">
|
<portal selector="#closet-list">
|
||||||
<div class="card card-primary card-tabs">
|
<div class="card card-primary card-tabs">
|
||||||
<div class="card-header p-0 pt-1 pl-1">
|
<div class="card-header p-0 pt-1 pl-1">
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<ul class="nav nav-tabs" role="tablist">
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
:class="{ active: category === 'skin' }"
|
:class="{ active: category === 'skin' }"
|
||||||
href="#"
|
href="#"
|
||||||
data-toggle="pill"
|
data-toggle="pill"
|
||||||
role="tab"
|
role="tab"
|
||||||
@click="switchCategory"
|
@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>
|
{{ $t('general.skin') }}
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
<div class="card-body">
|
<li class="nav-item">
|
||||||
<div
|
<a
|
||||||
v-if="category === 'skin'"
|
class="nav-link"
|
||||||
id="skin-category"
|
:class="{ active: category === 'cape' }"
|
||||||
:class="{ active: category === 'skin' }"
|
href="#"
|
||||||
>
|
@click="switchCategory"
|
||||||
<div v-if="skinItems.length === 0" class="text-center p-3">
|
>
|
||||||
<div v-if="query !== ''" v-t="'general.noResult'" />
|
{{ $t('general.cape') }}
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
</a>
|
||||||
<div v-else v-html="$t('user.emptyClosetMsg', { url: linkToSkin })" />
|
</li>
|
||||||
</div>
|
<li class="nav-item d-none d-md-block">
|
||||||
<div v-else class="d-flex flex-wrap">
|
<a
|
||||||
<closet-item
|
v-t="'user.closet.upload'"
|
||||||
v-for="(item, index) in skinItems"
|
:href="`${baseUrl}/skinlib/upload`"
|
||||||
:key="item.tid"
|
class="nav-link"
|
||||||
:tid="item.tid"
|
|
||||||
:name="item.name"
|
|
||||||
:type="item.type"
|
|
||||||
:selected="selectedSkin === item.tid"
|
|
||||||
@select="selectTexture(item.tid)"
|
|
||||||
@item-removed="removeSkinItem(index)"
|
|
||||||
/>
|
/>
|
||||||
</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>
|
||||||
<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>
|
||||||
</div>
|
<div class="card-body">
|
||||||
|
<div
|
||||||
<div class="col-md-4">
|
v-if="category === 'skin'"
|
||||||
<previewer
|
id="skin-category"
|
||||||
closet-mode
|
:class="{ active: category === 'skin' }"
|
||||||
:skin="skinUrl"
|
>
|
||||||
:cape="capeUrl"
|
<div v-if="skinItems.length === 0" class="text-center p-3">
|
||||||
:model="model"
|
<div v-if="query !== ''" v-t="'general.noResult'" />
|
||||||
>
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<template #footer>
|
<div v-else v-html="$t('user.emptyClosetMsg', { url: linkToSkin })" />
|
||||||
<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>
|
</div>
|
||||||
</template>
|
<div v-else class="d-flex flex-wrap">
|
||||||
</previewer>
|
<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>
|
||||||
</div>
|
</portal>
|
||||||
<apply-to-player-dialog ref="useAs" :skin="selectedSkin" :cape="selectedCape" />
|
|
||||||
<add-player-dialog @add="fetchPlayersList" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Paginate from 'vuejs-paginate'
|
import Paginate from 'vuejs-paginate'
|
||||||
import { debounce, queryString } from '../../scripts/utils'
|
import { debounce, queryString } from '../../scripts/utils'
|
||||||
|
import Portal from '../../components/Portal'
|
||||||
import ClosetItem from '../../components/ClosetItem.vue'
|
import ClosetItem from '../../components/ClosetItem.vue'
|
||||||
import EmailVerification from '../../components/EmailVerification.vue'
|
import EmailVerification from '../../components/EmailVerification.vue'
|
||||||
import AddPlayerDialog from '../../components/AddPlayerDialog.vue'
|
import AddPlayerDialog from '../../components/AddPlayerDialog.vue'
|
||||||
|
|
@ -182,6 +185,7 @@ import emitMounted from '../../components/mixins/emitMounted'
|
||||||
export default {
|
export default {
|
||||||
name: 'Closet',
|
name: 'Closet',
|
||||||
components: {
|
components: {
|
||||||
|
Portal,
|
||||||
Paginate,
|
Paginate,
|
||||||
ClosetItem,
|
ClosetItem,
|
||||||
Previewer: () => import('../../components/Previewer.vue'),
|
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 ClosetItem from '@/components/ClosetItem.vue'
|
||||||
import Previewer from '@/components/Previewer.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', () => {
|
test('fetch closet data before mount', () => {
|
||||||
Vue.prototype.$http.get.mockResolvedValue({ data: {} })
|
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 title %}{{ trans('general.my-closet') }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{{ include('shared.grid') }}
|
||||||
|
|
||||||
|
<div id="modals"></div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block before_foot %}
|
{% block before_foot %}
|
||||||
<script>
|
<script>
|
||||||
Object.defineProperty(blessing, 'extra', {
|
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