mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2026-03-01 12:44:37 +00:00
fix(webapp): fix badge select + drag&drop for badges on desktop devices (#9287)
This commit is contained in:
parent
ccf10610c8
commit
9ff74b4219
@ -1,4 +1,4 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/vue'
|
||||
import { render, screen, fireEvent, within } from '@testing-library/vue'
|
||||
import BadgeSelection from './BadgeSelection.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -72,5 +72,78 @@ describe('Badges.vue', () => {
|
||||
expect(wrapper.emitted()['badge-selected']).toEqual([[badges[1]], [null]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('with drag enabled', () => {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ badges, dragEnabled: true })
|
||||
})
|
||||
|
||||
it('items are draggable', () => {
|
||||
const items = wrapper.container.querySelectorAll('.badge-selection-item')
|
||||
items.forEach((item) => {
|
||||
expect(item.getAttribute('draggable')).toBe('true')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dragstart on an item', () => {
|
||||
it('sets dataTransfer data', async () => {
|
||||
const item = within(wrapper.container)
|
||||
.getByText(badges[0].description)
|
||||
.closest('.badge-selection-item')
|
||||
const setData = jest.fn()
|
||||
await fireEvent.dragStart(item, {
|
||||
dataTransfer: { setData, effectAllowed: '' },
|
||||
})
|
||||
expect(setData).toHaveBeenCalledWith(
|
||||
'application/json',
|
||||
JSON.stringify({ source: 'reserve', badge: badges[0] }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('drop on container with hex badge data', () => {
|
||||
it('emits badge-returned', async () => {
|
||||
const container = wrapper.container.querySelector('.badge-selection')
|
||||
const hexData = JSON.stringify({ source: 'hex', index: 2, badge: { id: '10' } })
|
||||
await fireEvent.drop(container, {
|
||||
dataTransfer: { getData: () => hexData },
|
||||
})
|
||||
expect(wrapper.emitted()['badge-returned']).toBeTruthy()
|
||||
expect(wrapper.emitted()['badge-returned'][0][0]).toEqual({
|
||||
source: 'hex',
|
||||
index: 2,
|
||||
badge: { id: '10' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('drop on container with reserve badge data', () => {
|
||||
it('does not emit badge-returned', async () => {
|
||||
const container = wrapper.container.querySelector('.badge-selection')
|
||||
const reserveData = JSON.stringify({ source: 'reserve', badge: badges[0] })
|
||||
await fireEvent.drop(container, {
|
||||
dataTransfer: { getData: () => reserveData },
|
||||
})
|
||||
expect(wrapper.emitted()['badge-returned']).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with drag disabled', () => {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ badges, dragEnabled: false })
|
||||
})
|
||||
|
||||
it('items are not draggable', () => {
|
||||
const items = wrapper.container.querySelectorAll('.badge-selection-item')
|
||||
items.forEach((item) => {
|
||||
expect(item.getAttribute('draggable')).toBe('false')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,10 +1,21 @@
|
||||
<template>
|
||||
<div class="badge-selection">
|
||||
<div
|
||||
class="badge-selection"
|
||||
:class="{ 'reserve-drag-over': reserveDragOver }"
|
||||
@dragover.prevent
|
||||
@dragenter="handleContainerDragEnter"
|
||||
@dragleave="handleContainerDragLeave"
|
||||
@drop.prevent="handleContainerDrop"
|
||||
>
|
||||
<button
|
||||
v-for="(badge, index) in badges"
|
||||
:key="badge.id"
|
||||
class="badge-selection-item"
|
||||
:class="{ dragging: draggingIndex === index }"
|
||||
:draggable="dragEnabled"
|
||||
@click="handleBadgeClick(badge, index)"
|
||||
@dragstart="handleItemDragStart($event, badge, index)"
|
||||
@dragend="handleItemDragEnd"
|
||||
>
|
||||
<div class="badge-icon">
|
||||
<img :src="backendPath(badge.icon)" :alt="badge.id" />
|
||||
@ -25,10 +36,17 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
dragEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: null,
|
||||
draggingIndex: null,
|
||||
reserveDragOver: false,
|
||||
dragEnterCount: 0,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -43,6 +61,37 @@ export default {
|
||||
this.selectedIndex = index
|
||||
this.$emit('badge-selected', badge)
|
||||
},
|
||||
handleItemDragStart(event, badge, index) {
|
||||
this.draggingIndex = index
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.setData('application/json', JSON.stringify({ source: 'reserve', badge }))
|
||||
},
|
||||
handleItemDragEnd() {
|
||||
this.draggingIndex = null
|
||||
},
|
||||
handleContainerDragEnter() {
|
||||
this.dragEnterCount++
|
||||
this.reserveDragOver = true
|
||||
},
|
||||
handleContainerDragLeave() {
|
||||
this.dragEnterCount--
|
||||
if (this.dragEnterCount <= 0) {
|
||||
this.dragEnterCount = 0
|
||||
this.reserveDragOver = false
|
||||
}
|
||||
},
|
||||
handleContainerDrop(event) {
|
||||
this.dragEnterCount = 0
|
||||
this.reserveDragOver = false
|
||||
try {
|
||||
const data = JSON.parse(event.dataTransfer.getData('application/json'))
|
||||
if (data.source === 'hex') {
|
||||
this.$emit('badge-returned', data)
|
||||
}
|
||||
} catch {
|
||||
// ignore invalid drag data
|
||||
}
|
||||
},
|
||||
resetSelection() {
|
||||
this.selectedIndex = null
|
||||
},
|
||||
@ -55,6 +104,17 @@ export default {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
|
||||
&.reserve-drag-over {
|
||||
border-color: $color-success;
|
||||
border-style: dashed;
|
||||
background-color: rgba($color-success, 0.05);
|
||||
}
|
||||
|
||||
.badge-selection-item {
|
||||
display: flex;
|
||||
@ -62,6 +122,10 @@ export default {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
background-color: #f5f5f5;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
@ -72,6 +136,10 @@ export default {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
|
||||
@ -111,5 +111,97 @@ describe('Badges.vue', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with drag enabled', () => {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ badges, selectionMode: true, dragEnabled: true })
|
||||
})
|
||||
|
||||
it('renders draggable badges except first and default ones', () => {
|
||||
const containers = wrapper.container.querySelectorAll('.hc-badge-container')
|
||||
expect(containers[0].getAttribute('draggable')).toBe('false')
|
||||
expect(containers[1].getAttribute('draggable')).toBe('false') // isDefault: true
|
||||
expect(containers[2].getAttribute('draggable')).toBe('true')
|
||||
})
|
||||
|
||||
it('first badge (index 0) is never draggable', () => {
|
||||
const firstBadge = wrapper.container.querySelector('.hc-badge-container')
|
||||
expect(firstBadge.getAttribute('draggable')).toBe('false')
|
||||
})
|
||||
|
||||
describe('dragstart on non-default badge', () => {
|
||||
it('emits drag-start with badge data', async () => {
|
||||
const badge = screen.getByTitle(badges[2].description)
|
||||
const container = badge.closest('.hc-badge-container')
|
||||
await fireEvent.dragStart(container, {
|
||||
dataTransfer: {
|
||||
setData: jest.fn(),
|
||||
effectAllowed: '',
|
||||
},
|
||||
})
|
||||
expect(wrapper.emitted()['drag-start']).toBeTruthy()
|
||||
expect(wrapper.emitted()['drag-start'][0][0]).toEqual({
|
||||
source: 'hex',
|
||||
index: 2,
|
||||
badge: badges[2],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('drop on a slot', () => {
|
||||
it('emits badge-drop with parsed source and target data', async () => {
|
||||
const targetBadge = screen.getByTitle(badges[2].description)
|
||||
const container = targetBadge.closest('.hc-badge-container')
|
||||
const sourceData = JSON.stringify({ source: 'reserve', badge: { id: '99', icon: '/x' } })
|
||||
await fireEvent.drop(container, {
|
||||
dataTransfer: {
|
||||
getData: () => sourceData,
|
||||
},
|
||||
})
|
||||
expect(wrapper.emitted()['badge-drop']).toBeTruthy()
|
||||
const emitted = wrapper.emitted()['badge-drop'][0][0]
|
||||
expect(emitted.targetIndex).toBe(2)
|
||||
expect(emitted.targetBadge).toEqual(badges[2])
|
||||
expect(emitted.source).toEqual({ source: 'reserve', badge: { id: '99', icon: '/x' } })
|
||||
})
|
||||
|
||||
it('does not emit badge-drop when dropping on index 0', async () => {
|
||||
const firstBadge = screen.getByTitle(badges[0].description)
|
||||
const container = firstBadge.closest('.hc-badge-container')
|
||||
await fireEvent.drop(container, {
|
||||
dataTransfer: {
|
||||
getData: () => '{}',
|
||||
},
|
||||
})
|
||||
expect(wrapper.emitted()['badge-drop']).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('dragend', () => {
|
||||
it('emits drag-end', async () => {
|
||||
const badge = screen.getByTitle(badges[2].description)
|
||||
const container = badge.closest('.hc-badge-container')
|
||||
await fireEvent.dragEnd(container)
|
||||
expect(wrapper.emitted()['drag-end']).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with drag disabled', () => {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper({ badges, selectionMode: true, dragEnabled: false })
|
||||
})
|
||||
|
||||
it('no badges are draggable', () => {
|
||||
const containers = wrapper.container.querySelectorAll('.hc-badge-container')
|
||||
containers.forEach((container) => {
|
||||
expect(container.getAttribute('draggable')).toBe('false')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -5,8 +5,19 @@
|
||||
class="hc-badge-container"
|
||||
v-for="(badge, index) in badges"
|
||||
:key="index"
|
||||
:class="{ selectable: selectionMode && index > 0, selected: selectedIndex === index }"
|
||||
:class="{
|
||||
selectable: selectionMode && index > 0,
|
||||
selected: selectedIndex === index,
|
||||
dragging: draggingIndex === index,
|
||||
'drag-over': dragOverIndex === index,
|
||||
}"
|
||||
:draggable="isDraggable(index, badge)"
|
||||
@click="handleBadgeClick(index)"
|
||||
@dragstart="handleDragStart($event, index, badge)"
|
||||
@dragend="handleDragEnd"
|
||||
@dragover.prevent="handleDragOver(index)"
|
||||
@dragleave="handleDragLeave(index)"
|
||||
@drop.prevent="handleDrop($event, index)"
|
||||
>
|
||||
<img :title="badge.description" :src="backendPath(badge.icon)" class="hc-badge" />
|
||||
</component>
|
||||
@ -26,14 +37,25 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
dragEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: null,
|
||||
draggingIndex: null,
|
||||
dragOverIndex: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
backendPath,
|
||||
isDraggable(index, badge) {
|
||||
if (!this.dragEnabled || index === 0) return false
|
||||
if (badge.isDefault) return false
|
||||
return true
|
||||
},
|
||||
handleBadgeClick(index) {
|
||||
if (!this.selectionMode || index === 0) {
|
||||
return
|
||||
@ -47,6 +69,46 @@ export default {
|
||||
this.selectedIndex = index
|
||||
this.$emit('badge-selected', index)
|
||||
},
|
||||
handleDragStart(event, index, badge) {
|
||||
if (!this.isDraggable(index, badge)) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
this.draggingIndex = index
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.setData(
|
||||
'application/json',
|
||||
JSON.stringify({ source: 'hex', index, badge }),
|
||||
)
|
||||
this.$emit('drag-start', { source: 'hex', index, badge })
|
||||
},
|
||||
handleDragEnd() {
|
||||
this.draggingIndex = null
|
||||
this.dragOverIndex = null
|
||||
this.$emit('drag-end')
|
||||
},
|
||||
handleDragOver(index) {
|
||||
if (index === 0) return
|
||||
this.dragOverIndex = index
|
||||
},
|
||||
handleDragLeave(index) {
|
||||
if (this.dragOverIndex === index) {
|
||||
this.dragOverIndex = null
|
||||
}
|
||||
},
|
||||
handleDrop(event, index) {
|
||||
if (index === 0) return
|
||||
this.dragOverIndex = null
|
||||
const badge = this.badges[index]
|
||||
let source = null
|
||||
try {
|
||||
source = JSON.parse(event.dataTransfer.getData('application/json'))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!source) return
|
||||
this.$emit('badge-drop', { source, targetIndex: index, targetBadge: badge })
|
||||
},
|
||||
resetSelection() {
|
||||
this.selectedIndex = null
|
||||
},
|
||||
@ -99,6 +161,16 @@ export default {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&.drag-over {
|
||||
filter: drop-shadow(0 0 4px $color-success);
|
||||
transform: scale(1.15);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.hc-badge {
|
||||
|
||||
@ -82,6 +82,7 @@ exports[`UserTeaserPopover given a non-touch device does not show button when us
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -91,6 +92,7 @@ exports[`UserTeaserPopover given a non-touch device does not show button when us
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -100,6 +102,7 @@ exports[`UserTeaserPopover given a non-touch device does not show button when us
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -109,6 +112,7 @@ exports[`UserTeaserPopover given a non-touch device does not show button when us
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -193,6 +197,7 @@ exports[`UserTeaserPopover given a touch device does not show button when userLi
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -202,6 +207,7 @@ exports[`UserTeaserPopover given a touch device does not show button when userLi
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -211,6 +217,7 @@ exports[`UserTeaserPopover given a touch device does not show button when userLi
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -220,6 +227,7 @@ exports[`UserTeaserPopover given a touch device does not show button when userLi
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -304,6 +312,7 @@ exports[`UserTeaserPopover given a touch device shows button when userLink is pr
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -313,6 +322,7 @@ exports[`UserTeaserPopover given a touch device shows button when userLink is pr
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -322,6 +332,7 @@ exports[`UserTeaserPopover given a touch device shows button when userLink is pr
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -331,6 +342,7 @@ exports[`UserTeaserPopover given a touch device shows button when userLink is pr
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -415,6 +427,7 @@ exports[`UserTeaserPopover renders correctly for a fresh user with zero counts 1
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -424,6 +437,7 @@ exports[`UserTeaserPopover renders correctly for a fresh user with zero counts 1
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -433,6 +447,7 @@ exports[`UserTeaserPopover renders correctly for a fresh user with zero counts 1
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -442,6 +457,7 @@ exports[`UserTeaserPopover renders correctly for a fresh user with zero counts 1
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -526,6 +542,7 @@ exports[`UserTeaserPopover shows badges when enabled 1`] = `
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -535,6 +552,7 @@ exports[`UserTeaserPopover shows badges when enabled 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -544,6 +562,7 @@ exports[`UserTeaserPopover shows badges when enabled 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -553,6 +572,7 @@ exports[`UserTeaserPopover shows badges when enabled 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
|
||||
@ -7,6 +7,7 @@ exports[`Badges.vue with badges renders 1`] = `
|
||||
>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
draggable="false"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
@ -29,6 +30,7 @@ exports[`Badges.vue with badges renders 1`] = `
|
||||
</button>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
draggable="false"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
@ -51,6 +53,7 @@ exports[`Badges.vue with badges renders 1`] = `
|
||||
</button>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
draggable="false"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
|
||||
@ -8,6 +8,7 @@ exports[`Badges.vue with badges in presentation mode renders 1`] = `
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -17,6 +18,7 @@ exports[`Badges.vue with badges in presentation mode renders 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -26,6 +28,7 @@ exports[`Badges.vue with badges in presentation mode renders 1`] = `
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -45,6 +48,7 @@ exports[`Badges.vue with badges in selection mode clicking on second badge selec
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -54,6 +58,7 @@ exports[`Badges.vue with badges in selection mode clicking on second badge selec
|
||||
</div>
|
||||
<button
|
||||
class="hc-badge-container selectable selected"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -63,6 +68,7 @@ exports[`Badges.vue with badges in selection mode clicking on second badge selec
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -82,6 +88,7 @@ exports[`Badges.vue with badges in selection mode clicking twice on second badge
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -91,6 +98,7 @@ exports[`Badges.vue with badges in selection mode clicking twice on second badge
|
||||
</div>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -100,6 +108,7 @@ exports[`Badges.vue with badges in selection mode clicking twice on second badge
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -119,6 +128,7 @@ exports[`Badges.vue with badges in selection mode renders 1`] = `
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -128,6 +138,7 @@ exports[`Badges.vue with badges in selection mode renders 1`] = `
|
||||
</div>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -137,6 +148,7 @@ exports[`Badges.vue with badges in selection mode renders 1`] = `
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
|
||||
@ -1050,10 +1050,15 @@
|
||||
"click-to-select": "Klicke auf einen freien Platz, um eine Badge hinzufügen.",
|
||||
"click-to-use": "Klicke auf eine Badge, um sie zu platzieren.",
|
||||
"description": "Hier hast du die Möglichkeit zu entscheiden, wie deine bereits erworbenen Badges in deinem Profil gezeigt werden sollen.",
|
||||
"drag-instruction": "Badges per Drag & Drop verschieben.",
|
||||
"drop-to-remove": "Hier ablegen, um die Badge zu entfernen.",
|
||||
"error-update": "Beim Aktualisieren deiner Badges ist ein Fehler aufgetreten.",
|
||||
"name": "Badges",
|
||||
"no-badges-available": "Im Moment stehen dir keine Badges zur Verfügung, die du hinzufügen könntest.",
|
||||
"remove": "Badge entfernen",
|
||||
"reserve-title": "Verfügbare Badges",
|
||||
"success-update": "Deine Badges wurden erfolgreich gespeichert.",
|
||||
"swap-partial-error": "Der Badge-Tausch konnte nicht abgeschlossen werden. Bitte versuche es erneut.",
|
||||
"verification": "Dies ist deine Verifikations-Badge und kann nicht geändert werden."
|
||||
},
|
||||
"blocked-users": {
|
||||
|
||||
@ -1050,10 +1050,15 @@
|
||||
"click-to-select": "Click on an empty space to add a badge.",
|
||||
"click-to-use": "Click on a badge to use it in the selected slot.",
|
||||
"description": "Here you can choose how to display your earned badges on your profile.",
|
||||
"drag-instruction": "Drag and drop badges to rearrange them.",
|
||||
"drop-to-remove": "Drop here to remove the badge.",
|
||||
"error-update": "An error occurred while updating your badges.",
|
||||
"name": "Badges",
|
||||
"no-badges-available": "You currently don't have any badges available to add.",
|
||||
"remove": "Remove Badge",
|
||||
"reserve-title": "Available Badges",
|
||||
"success-update": "Your badges have been updated successfully.",
|
||||
"swap-partial-error": "The badge swap could not be completed. Please try again.",
|
||||
"verification": "This is your verification badge and cannot be changed."
|
||||
},
|
||||
"blocked-users": {
|
||||
|
||||
@ -1047,14 +1047,19 @@
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
"click-to-select": "Haz clic en un espacio vacío para añadir una insignia.",
|
||||
"click-to-use": "Haz clic en una insignia para colocarla en el espacio seleccionado.",
|
||||
"description": "Aquí puedes elegir cómo mostrar tus insignias obtenidas en tu perfil.",
|
||||
"drag-instruction": "Arrastra y suelta las insignias para reorganizarlas.",
|
||||
"drop-to-remove": "Suelta aquí para quitar la insignia.",
|
||||
"error-update": "Se produjo un error al actualizar tus insignias.",
|
||||
"name": "Insignias",
|
||||
"no-badges-available": "Actualmente no tienes insignias disponibles para añadir.",
|
||||
"remove": "Quitar insignia",
|
||||
"reserve-title": "Insignias disponibles",
|
||||
"success-update": "Tus insignias se han actualizado correctamente.",
|
||||
"swap-partial-error": "El intercambio de insignias no se pudo completar. Inténtalo de nuevo.",
|
||||
"verification": "Esta es tu insignia de verificación y no se puede cambiar."
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": "Bloquear usuario",
|
||||
|
||||
@ -1047,14 +1047,19 @@
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
"click-to-select": "Cliquez sur un emplacement vide pour ajouter un badge.",
|
||||
"click-to-use": "Cliquez sur un badge pour le placer dans l'emplacement sélectionné.",
|
||||
"description": "Ici, vous pouvez choisir comment afficher vos badges obtenus sur votre profil.",
|
||||
"drag-instruction": "Glissez-déposez les badges pour les réorganiser.",
|
||||
"drop-to-remove": "Déposez ici pour retirer le badge.",
|
||||
"error-update": "Une erreur est survenue lors de la mise à jour de vos badges.",
|
||||
"name": "Badges",
|
||||
"no-badges-available": "Vous n'avez actuellement aucun badge disponible à ajouter.",
|
||||
"remove": "Retirer le badge",
|
||||
"reserve-title": "Badges disponibles",
|
||||
"success-update": "Vos badges ont été mis à jour avec succès.",
|
||||
"swap-partial-error": "L'échange de badges n'a pas pu être effectué. Veuillez réessayer.",
|
||||
"verification": "Ceci est votre badge de vérification et ne peut pas être modifié."
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": "Bloquer l'utilisateur",
|
||||
|
||||
@ -1047,14 +1047,19 @@
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
"click-to-select": "Clicca su uno spazio vuoto per aggiungere un badge.",
|
||||
"click-to-use": "Clicca su un badge per posizionarlo nello spazio selezionato.",
|
||||
"description": "Qui puoi scegliere come visualizzare i tuoi badge ottenuti sul tuo profilo.",
|
||||
"drag-instruction": "Trascina i badge per riorganizzarli.",
|
||||
"drop-to-remove": "Rilascia qui per rimuovere il badge.",
|
||||
"error-update": "Si è verificato un errore durante l'aggiornamento dei tuoi badge.",
|
||||
"name": "Badge",
|
||||
"no-badges-available": "Al momento non hai badge disponibili da aggiungere.",
|
||||
"remove": "Rimuovi badge",
|
||||
"reserve-title": "Badge disponibili",
|
||||
"success-update": "I tuoi badge sono stati aggiornati con successo.",
|
||||
"swap-partial-error": "Lo scambio dei badge non è stato completato. Riprova.",
|
||||
"verification": "Questo è il tuo badge di verifica e non può essere modificato."
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": null,
|
||||
|
||||
@ -1047,14 +1047,19 @@
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
"click-to-select": "Klik op een lege plek om een badge toe te voegen.",
|
||||
"click-to-use": "Klik op een badge om deze in de geselecteerde plek te plaatsen.",
|
||||
"description": "Hier kun je kiezen hoe je verdiende badges op je profiel worden weergegeven.",
|
||||
"drag-instruction": "Sleep badges om ze te herschikken.",
|
||||
"drop-to-remove": "Laat hier los om de badge te verwijderen.",
|
||||
"error-update": "Er is een fout opgetreden bij het bijwerken van je badges.",
|
||||
"name": "Badges",
|
||||
"no-badges-available": "Je hebt momenteel geen badges beschikbaar om toe te voegen.",
|
||||
"remove": "Badge verwijderen",
|
||||
"reserve-title": "Beschikbare badges",
|
||||
"success-update": "Je badges zijn succesvol bijgewerkt.",
|
||||
"swap-partial-error": "Het wisselen van badges kon niet worden voltooid. Probeer het opnieuw.",
|
||||
"verification": "Dit is je verificatiebadge en kan niet worden gewijzigd."
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": null,
|
||||
|
||||
@ -1047,14 +1047,19 @@
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
"click-to-select": "Kliknij na puste miejsce, aby dodać odznakę.",
|
||||
"click-to-use": "Kliknij na odznakę, aby umieścić ją w wybranym miejscu.",
|
||||
"description": "Tutaj możesz wybrać, jak wyświetlać zdobyte odznaki na swoim profilu.",
|
||||
"drag-instruction": "Przeciągnij i upuść odznaki, aby je uporządkować.",
|
||||
"drop-to-remove": "Upuść tutaj, aby usunąć odznakę.",
|
||||
"error-update": "Wystąpił błąd podczas aktualizacji odznak.",
|
||||
"name": "Odznaki",
|
||||
"no-badges-available": "Obecnie nie masz żadnych dostępnych odznak do dodania.",
|
||||
"remove": "Usuń odznakę",
|
||||
"reserve-title": "Dostępne odznaki",
|
||||
"success-update": "Twoje odznaki zostały pomyślnie zaktualizowane.",
|
||||
"swap-partial-error": "Zamiana odznak nie mogła zostać ukończona. Spróbuj ponownie.",
|
||||
"verification": "To jest Twoja odznaka weryfikacyjna i nie można jej zmienić."
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": null,
|
||||
|
||||
@ -1047,14 +1047,19 @@
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
"click-to-select": "Clique num espaço vazio para adicionar uma medalha.",
|
||||
"click-to-use": "Clique numa medalha para a colocar no espaço selecionado.",
|
||||
"description": "Aqui pode escolher como exibir as suas medalhas conquistadas no seu perfil.",
|
||||
"drag-instruction": "Arraste e solte as medalhas para reorganizá-las.",
|
||||
"drop-to-remove": "Solte aqui para remover a medalha.",
|
||||
"error-update": "Ocorreu um erro ao atualizar as suas medalhas.",
|
||||
"name": "Medalhas",
|
||||
"no-badges-available": "De momento não tem medalhas disponíveis para adicionar.",
|
||||
"remove": "Remover medalha",
|
||||
"reserve-title": "Medalhas disponíveis",
|
||||
"success-update": "As suas medalhas foram atualizadas com sucesso.",
|
||||
"swap-partial-error": "A troca de medalhas não pôde ser concluída. Tente novamente.",
|
||||
"verification": "Esta é a sua medalha de verificação e não pode ser alterada."
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": "Bloquear usuário",
|
||||
|
||||
@ -1047,14 +1047,19 @@
|
||||
},
|
||||
"settings": {
|
||||
"badges": {
|
||||
"click-to-select": null,
|
||||
"click-to-use": null,
|
||||
"description": null,
|
||||
"name": null,
|
||||
"no-badges-available": null,
|
||||
"remove": null,
|
||||
"success-update": null,
|
||||
"verification": null
|
||||
"click-to-select": "Нажмите на пустое место, чтобы добавить награду.",
|
||||
"click-to-use": "Нажмите на награду, чтобы поместить её в выбранное место.",
|
||||
"description": "Здесь вы можете выбрать, как отображать полученные награды в вашем профиле.",
|
||||
"drag-instruction": "Перетаскивайте награды, чтобы изменить их расположение.",
|
||||
"drop-to-remove": "Перетащите сюда, чтобы убрать награду.",
|
||||
"error-update": "При обновлении наград произошла ошибка.",
|
||||
"name": "Награды",
|
||||
"no-badges-available": "В настоящее время у вас нет доступных наград для добавления.",
|
||||
"remove": "Убрать награду",
|
||||
"reserve-title": "Доступные награды",
|
||||
"success-update": "Ваши награды были успешно обновлены.",
|
||||
"swap-partial-error": "Обмен наградами не удалось завершить. Попробуйте ещё раз.",
|
||||
"verification": "Это ваша награда за верификацию, её нельзя изменить."
|
||||
},
|
||||
"blocked-users": {
|
||||
"block": "Блокировать",
|
||||
|
||||
@ -970,6 +970,7 @@ exports[`ProfileSlug given an authenticated user given another profile user and
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -979,6 +980,7 @@ exports[`ProfileSlug given an authenticated user given another profile user and
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -988,6 +990,7 @@ exports[`ProfileSlug given an authenticated user given another profile user and
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -2213,6 +2216,7 @@ exports[`ProfileSlug given an authenticated user given the logged in user as pro
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -2222,6 +2226,7 @@ exports[`ProfileSlug given an authenticated user given the logged in user as pro
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -2231,6 +2236,7 @@ exports[`ProfileSlug given an authenticated user given the logged in user as pro
|
||||
</div>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
|
||||
@ -16,7 +16,7 @@ exports[`badge settings with badges more badges available selecting an empty slo
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="ds-mb-small ds-mt-base ds-space-centered"
|
||||
class="ds-mb-small ds-mt-base badge-content"
|
||||
>
|
||||
<div
|
||||
class="presenterContainer"
|
||||
@ -26,6 +26,7 @@ exports[`badge settings with badges more badges available selecting an empty slo
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -35,6 +36,7 @@ exports[`badge settings with badges more badges available selecting an empty slo
|
||||
</div>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="true"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -44,6 +46,7 @@ exports[`badge settings with badges more badges available selecting an empty slo
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable selected"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -53,6 +56,7 @@ exports[`badge settings with badges more badges available selecting an empty slo
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="true"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -63,6 +67,14 @@ exports[`badge settings with badges more badges available selecting an empty slo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="drag-instruction"
|
||||
>
|
||||
|
||||
settings.badges.drag-instruction
|
||||
|
||||
</p>
|
||||
|
||||
<!---->
|
||||
|
||||
<div>
|
||||
@ -78,11 +90,20 @@ exports[`badge settings with badges more badges available selecting an empty slo
|
||||
<div
|
||||
class="selection-info"
|
||||
>
|
||||
<h3
|
||||
class="reserve-title"
|
||||
>
|
||||
|
||||
settings.badges.reserve-title
|
||||
|
||||
</h3>
|
||||
|
||||
<div
|
||||
class="badge-selection"
|
||||
>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
draggable="true"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
@ -105,6 +126,7 @@ exports[`badge settings with badges more badges available selecting an empty slo
|
||||
</button>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
draggable="true"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
@ -127,6 +149,8 @@ exports[`badge settings with badges more badges available selecting an empty slo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -148,7 +172,7 @@ exports[`badge settings with badges no more badges available selecting an empty
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="ds-mb-small ds-mt-base ds-space-centered"
|
||||
class="ds-mb-small ds-mt-base badge-content"
|
||||
>
|
||||
<div
|
||||
class="presenterContainer"
|
||||
@ -158,6 +182,7 @@ exports[`badge settings with badges no more badges available selecting an empty
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -167,6 +192,7 @@ exports[`badge settings with badges no more badges available selecting an empty
|
||||
</div>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="true"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -176,6 +202,7 @@ exports[`badge settings with badges no more badges available selecting an empty
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable selected"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -185,6 +212,7 @@ exports[`badge settings with badges no more badges available selecting an empty
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="true"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -195,6 +223,14 @@ exports[`badge settings with badges no more badges available selecting an empty
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="drag-instruction"
|
||||
>
|
||||
|
||||
settings.badges.drag-instruction
|
||||
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
settings.badges.no-badges-available
|
||||
@ -206,6 +242,8 @@ exports[`badge settings with badges no more badges available selecting an empty
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -227,7 +265,7 @@ exports[`badge settings with badges renders 1`] = `
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="ds-mb-small ds-mt-base ds-space-centered"
|
||||
class="ds-mb-small ds-mt-base badge-content"
|
||||
>
|
||||
<div
|
||||
class="presenterContainer"
|
||||
@ -237,6 +275,7 @@ exports[`badge settings with badges renders 1`] = `
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -246,6 +285,7 @@ exports[`badge settings with badges renders 1`] = `
|
||||
</div>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="true"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -255,6 +295,7 @@ exports[`badge settings with badges renders 1`] = `
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -264,6 +305,7 @@ exports[`badge settings with badges renders 1`] = `
|
||||
</button>
|
||||
<button
|
||||
class="hc-badge-container selectable"
|
||||
draggable="true"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -274,6 +316,14 @@ exports[`badge settings with badges renders 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="drag-instruction"
|
||||
>
|
||||
|
||||
settings.badges.drag-instruction
|
||||
|
||||
</p>
|
||||
|
||||
<!---->
|
||||
|
||||
<div>
|
||||
@ -286,6 +336,69 @@ exports[`badge settings with badges renders 1`] = `
|
||||
|
||||
<!---->
|
||||
|
||||
<div
|
||||
class="selection-info"
|
||||
>
|
||||
<h3
|
||||
class="reserve-title"
|
||||
>
|
||||
|
||||
settings.badges.reserve-title
|
||||
|
||||
</h3>
|
||||
|
||||
<div
|
||||
class="badge-selection"
|
||||
>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
draggable="true"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
>
|
||||
<img
|
||||
alt="4"
|
||||
src="/api/path/to/fourth/icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="badge-info"
|
||||
>
|
||||
<div
|
||||
class="badge-description"
|
||||
>
|
||||
Fourth description
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="badge-selection-item"
|
||||
draggable="true"
|
||||
>
|
||||
<div
|
||||
class="badge-icon"
|
||||
>
|
||||
<img
|
||||
alt="5"
|
||||
src="/api/path/to/fifth/icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="badge-info"
|
||||
>
|
||||
<div
|
||||
class="badge-description"
|
||||
>
|
||||
Fifth description
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
@ -308,7 +421,7 @@ exports[`badge settings without badges renders 1`] = `
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="ds-mb-small ds-mt-base ds-space-centered"
|
||||
class="ds-mb-small ds-mt-base badge-content"
|
||||
>
|
||||
<div
|
||||
class="presenterContainer"
|
||||
@ -318,6 +431,7 @@ exports[`badge settings without badges renders 1`] = `
|
||||
>
|
||||
<div
|
||||
class="hc-badge-container"
|
||||
draggable="false"
|
||||
>
|
||||
<img
|
||||
class="hc-badge"
|
||||
@ -328,6 +442,16 @@ exports[`badge settings without badges renders 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="drag-instruction"
|
||||
>
|
||||
|
||||
settings.badges.drag-instruction
|
||||
|
||||
</p>
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
@ -23,6 +23,7 @@ describe('badge settings', () => {
|
||||
$toast: {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
},
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
@ -158,6 +159,23 @@ describe('badge settings', () => {
|
||||
description: 'Third description',
|
||||
},
|
||||
],
|
||||
badgeTrophiesUnused: [
|
||||
{
|
||||
id: '1',
|
||||
icon: '/path/to/some/icon',
|
||||
description: 'Some description',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
icon: '/path/to/fourth/icon',
|
||||
description: 'Fourth description',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
icon: '/path/to/fifth/icon',
|
||||
description: 'Fifth description',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@ -188,6 +206,7 @@ describe('badge settings', () => {
|
||||
id: 'u23',
|
||||
badgeTrophiesSelected:
|
||||
removedResponseData.setTrophyBadgeSelected.badgeTrophiesSelected,
|
||||
badgeTrophiesUnused: removedResponseData.setTrophyBadgeSelected.badgeTrophiesUnused,
|
||||
}),
|
||||
)
|
||||
})
|
||||
@ -320,5 +339,254 @@ describe('badge settings', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('drag and drop', () => {
|
||||
const makeDropData = (source) => JSON.stringify(source)
|
||||
|
||||
describe('assign badge from reserve to empty slot via DnD', () => {
|
||||
const assignResponseData = {
|
||||
setTrophyBadgeSelected: {
|
||||
id: 'u23',
|
||||
badgeTrophiesSelected: [
|
||||
badgeTrophiesSelected[0],
|
||||
{
|
||||
id: '4',
|
||||
icon: '/path/to/fourth/icon',
|
||||
isDefault: false,
|
||||
description: 'Fourth description',
|
||||
},
|
||||
badgeTrophiesSelected[2],
|
||||
],
|
||||
badgeTrophiesUnused: [badgeTrophiesUnused[1]],
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockImplementation(({ update }) => {
|
||||
const result = { data: assignResponseData }
|
||||
if (update) update(null, result)
|
||||
return Promise.resolve(result)
|
||||
})
|
||||
|
||||
// Simulate dropping a reserve badge on the empty hex slot (index 2 in Badges = slot 1)
|
||||
const emptySlot = screen.getAllByTitle('Empty')[0]
|
||||
const container = emptySlot.closest('.hc-badge-container')
|
||||
const sourceData = makeDropData({
|
||||
source: 'reserve',
|
||||
badge: badgeTrophiesUnused[0],
|
||||
})
|
||||
await fireEvent.drop(container, {
|
||||
dataTransfer: { getData: () => sourceData },
|
||||
})
|
||||
})
|
||||
|
||||
it('calls the server with correct badge and slot', () => {
|
||||
expect(apolloMutateMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
badgeId: '4',
|
||||
slot: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows success message', () => {
|
||||
expect(mocks.$toast.success).toHaveBeenCalledWith('settings.badges.success-update')
|
||||
})
|
||||
})
|
||||
|
||||
describe('remove badge from hex to reserve via DnD', () => {
|
||||
const removeResponseData = {
|
||||
setTrophyBadgeSelected: {
|
||||
id: 'u23',
|
||||
badgeTrophiesSelected: [
|
||||
{
|
||||
id: 'empty-0',
|
||||
icon: '/path/to/empty/icon',
|
||||
isDefault: true,
|
||||
description: 'Empty',
|
||||
},
|
||||
badgeTrophiesSelected[1],
|
||||
badgeTrophiesSelected[2],
|
||||
],
|
||||
badgeTrophiesUnused: [badgeTrophiesSelected[0], ...badgeTrophiesUnused],
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockImplementation(({ update }) => {
|
||||
const result = { data: removeResponseData }
|
||||
if (update) update(null, result)
|
||||
return Promise.resolve(result)
|
||||
})
|
||||
|
||||
// Simulate dropping a hex badge on the reserve container
|
||||
const reserveContainer = wrapper.container.querySelector('.badge-selection')
|
||||
const hexData = makeDropData({
|
||||
source: 'hex',
|
||||
index: 1,
|
||||
badge: badgeTrophiesSelected[0],
|
||||
})
|
||||
await fireEvent.drop(reserveContainer, {
|
||||
dataTransfer: { getData: () => hexData },
|
||||
})
|
||||
})
|
||||
|
||||
it('calls the server to remove badge', () => {
|
||||
expect(apolloMutateMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
badgeId: null,
|
||||
slot: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows success message', () => {
|
||||
expect(mocks.$toast.success).toHaveBeenCalledWith('settings.badges.success-update')
|
||||
})
|
||||
})
|
||||
|
||||
describe('swap two badges via DnD', () => {
|
||||
const swapResponse1 = {
|
||||
setTrophyBadgeSelected: {
|
||||
id: 'u23',
|
||||
badgeTrophiesSelected: [
|
||||
{
|
||||
id: '3',
|
||||
icon: '/path/to/third/icon',
|
||||
isDefault: false,
|
||||
description: 'Third description',
|
||||
},
|
||||
badgeTrophiesSelected[1],
|
||||
{
|
||||
id: 'empty-temp',
|
||||
icon: '/path/to/empty/icon',
|
||||
isDefault: true,
|
||||
description: 'Empty',
|
||||
},
|
||||
],
|
||||
badgeTrophiesUnused: [badgeTrophiesSelected[0], ...badgeTrophiesUnused],
|
||||
},
|
||||
}
|
||||
|
||||
const swapResponse2 = {
|
||||
setTrophyBadgeSelected: {
|
||||
id: 'u23',
|
||||
badgeTrophiesSelected: [
|
||||
{
|
||||
id: '3',
|
||||
icon: '/path/to/third/icon',
|
||||
isDefault: false,
|
||||
description: 'Third description',
|
||||
},
|
||||
badgeTrophiesSelected[1],
|
||||
{
|
||||
id: '1',
|
||||
icon: '/path/to/some/icon',
|
||||
isDefault: false,
|
||||
description: 'Some description',
|
||||
},
|
||||
],
|
||||
badgeTrophiesUnused,
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
let callCount = 0
|
||||
apolloMutateMock.mockImplementation(({ update }) => {
|
||||
callCount++
|
||||
const responseData = callCount === 1 ? swapResponse1 : swapResponse2
|
||||
const result = { data: responseData }
|
||||
if (update) update(null, result)
|
||||
return Promise.resolve(result)
|
||||
})
|
||||
|
||||
// Simulate dragging badge at index 1 onto badge at index 3 (both occupied)
|
||||
const targetBadge = screen.getByTitle(badgeTrophiesSelected[2].description)
|
||||
const container = targetBadge.closest('.hc-badge-container')
|
||||
const sourceData = makeDropData({
|
||||
source: 'hex',
|
||||
index: 1,
|
||||
badge: badgeTrophiesSelected[0],
|
||||
})
|
||||
await fireEvent.drop(container, {
|
||||
dataTransfer: { getData: () => sourceData },
|
||||
})
|
||||
})
|
||||
|
||||
it('calls the server twice for swap', () => {
|
||||
expect(apolloMutateMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('first mutation moves source to target slot', () => {
|
||||
expect(apolloMutateMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
badgeId: '1',
|
||||
slot: 2,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('second mutation moves former target to source slot', () => {
|
||||
expect(apolloMutateMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
badgeId: '3',
|
||||
slot: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('shows success message', () => {
|
||||
expect(mocks.$toast.success).toHaveBeenCalledWith('settings.badges.success-update')
|
||||
})
|
||||
})
|
||||
|
||||
describe('swap with partial failure', () => {
|
||||
beforeEach(async () => {
|
||||
let callCount = 0
|
||||
apolloMutateMock.mockImplementation(({ update }) => {
|
||||
callCount++
|
||||
if (callCount === 1) {
|
||||
const result = {
|
||||
data: {
|
||||
setTrophyBadgeSelected: {
|
||||
id: 'u23',
|
||||
badgeTrophiesSelected: badgeTrophiesSelected,
|
||||
badgeTrophiesUnused: badgeTrophiesUnused,
|
||||
},
|
||||
},
|
||||
}
|
||||
if (update) update(null, result)
|
||||
return Promise.resolve(result)
|
||||
}
|
||||
return Promise.reject(new Error('Server error'))
|
||||
})
|
||||
|
||||
const targetBadge = screen.getByTitle(badgeTrophiesSelected[2].description)
|
||||
const container = targetBadge.closest('.hc-badge-container')
|
||||
const sourceData = makeDropData({
|
||||
source: 'hex',
|
||||
index: 1,
|
||||
badge: badgeTrophiesSelected[0],
|
||||
})
|
||||
await fireEvent.drop(container, {
|
||||
dataTransfer: { getData: () => sourceData },
|
||||
})
|
||||
})
|
||||
|
||||
it('shows swap partial error', () => {
|
||||
expect(mocks.$toast.error).toHaveBeenCalledWith('settings.badges.swap-partial-error')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,16 +2,24 @@
|
||||
<os-card>
|
||||
<h2 class="title">{{ $t('settings.badges.name') }}</h2>
|
||||
<p>{{ $t('settings.badges.description') }}</p>
|
||||
<div class="ds-mb-small ds-mt-base ds-space-centered">
|
||||
<div class="ds-mb-small ds-mt-base badge-content">
|
||||
<div class="presenterContainer">
|
||||
<badges
|
||||
:badges="[currentUser.badgeVerification, ...selectedBadges]"
|
||||
:selection-mode="true"
|
||||
:drag-enabled="dragSupported"
|
||||
@badge-selected="handleBadgeSlotSelection"
|
||||
@drag-start="handleHexDragStart"
|
||||
@drag-end="handleHexDragEnd"
|
||||
@badge-drop="handleBadgeDrop"
|
||||
ref="badgesComponent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="dragSupported" class="drag-instruction">
|
||||
{{ $t('settings.badges.drag-instruction') }}
|
||||
</p>
|
||||
|
||||
<p v-if="!availableBadges.length && isEmptySlotSelected">
|
||||
{{ $t('settings.badges.no-badges-available') }}
|
||||
</p>
|
||||
@ -39,16 +47,30 @@
|
||||
</os-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="availableBadges.length && selectedBadgeIndex !== null && isEmptySlotSelected"
|
||||
class="selection-info"
|
||||
>
|
||||
<div v-if="availableBadges.length > 0" class="selection-info">
|
||||
<h3 v-if="dragSupported || selectedBadgeIndex !== null" class="reserve-title">
|
||||
{{ $t('settings.badges.reserve-title') }}
|
||||
</h3>
|
||||
<badge-selection
|
||||
v-if="dragSupported || (selectedBadgeIndex !== null && isEmptySlotSelected)"
|
||||
:badges="availableBadges"
|
||||
:drag-enabled="dragSupported"
|
||||
@badge-selected="assignBadgeToSlot"
|
||||
@badge-returned="handleBadgeReturn"
|
||||
ref="badgeSelection"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isDraggingFromHex && !availableBadges.length"
|
||||
class="empty-reserve-drop-zone"
|
||||
@dragover.prevent="emptyReserveDragOver = true"
|
||||
@dragleave="emptyReserveDragOver = false"
|
||||
@drop.prevent="handleEmptyReserveDrop"
|
||||
:class="{ 'reserve-drag-over': emptyReserveDragOver }"
|
||||
>
|
||||
{{ $t('settings.badges.drop-to-remove') }}
|
||||
</div>
|
||||
</div>
|
||||
</os-card>
|
||||
</template>
|
||||
@ -67,6 +89,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
selectedBadgeIndex: null,
|
||||
isDraggingFromHex: false,
|
||||
isProcessingDrop: false,
|
||||
emptyReserveDragOver: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -85,11 +110,19 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.userBadges = [...(this.currentUser.badgeTrophiesSelected || [])]
|
||||
this.dragSupported = this.detectDragSupport()
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
setCurrentUser: 'auth/SET_USER',
|
||||
}),
|
||||
detectDragSupport() {
|
||||
if (typeof window === 'undefined') return false
|
||||
if (!window.matchMedia) return !('ontouchstart' in window)
|
||||
const hasFinePointer = window.matchMedia('(pointer: fine)').matches
|
||||
const isWideScreen = window.matchMedia('(min-width: 640px)').matches
|
||||
return hasFinePointer && isWideScreen
|
||||
},
|
||||
handleBadgeSlotSelection(index) {
|
||||
if (index === 0) {
|
||||
this.$toast.info(this.$t('settings.badges.verification'))
|
||||
@ -99,6 +132,83 @@ export default {
|
||||
|
||||
this.selectedBadgeIndex = index === null ? null : index - 1 // The first badge in badges component is the verification badge
|
||||
},
|
||||
handleHexDragStart() {
|
||||
this.isDraggingFromHex = true
|
||||
},
|
||||
handleHexDragEnd() {
|
||||
this.isDraggingFromHex = false
|
||||
},
|
||||
async handleBadgeDrop(dropData) {
|
||||
if (this.isProcessingDrop) return
|
||||
this.isProcessingDrop = true
|
||||
|
||||
try {
|
||||
const source = dropData.source
|
||||
const targetIndex = dropData.targetIndex - 1 // adjust for verification badge offset
|
||||
const targetBadge = dropData.targetBadge
|
||||
|
||||
if (source && source.source === 'reserve') {
|
||||
// Assign: Reserve → Slot
|
||||
await this.setSlot(source.badge, targetIndex)
|
||||
this.$toast.success(this.$t('settings.badges.success-update'))
|
||||
} else if (source && source.source === 'hex') {
|
||||
const sourceIndex = source.index - 1 // adjust for verification badge offset
|
||||
if (targetBadge.isDefault) {
|
||||
// Move to empty slot: 1 mutation
|
||||
await this.setSlot(source.badge, targetIndex)
|
||||
this.$toast.success(this.$t('settings.badges.success-update'))
|
||||
} else {
|
||||
// Swap: 2 mutations
|
||||
await this.swapBadges(source.badge, sourceIndex, targetBadge, targetIndex)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.$toast.error(this.$t('settings.badges.error-update'))
|
||||
} finally {
|
||||
this.isProcessingDrop = false
|
||||
this.isDraggingFromHex = false
|
||||
this.$refs.badgesComponent.resetSelection()
|
||||
this.selectedBadgeIndex = null
|
||||
}
|
||||
},
|
||||
async handleBadgeReturn(data) {
|
||||
if (this.isProcessingDrop) return
|
||||
this.isProcessingDrop = true
|
||||
|
||||
try {
|
||||
const sourceIndex = data.index - 1 // adjust for verification badge offset
|
||||
await this.setSlot(null, sourceIndex)
|
||||
this.$toast.success(this.$t('settings.badges.success-update'))
|
||||
} catch (error) {
|
||||
this.$toast.error(this.$t('settings.badges.error-update'))
|
||||
} finally {
|
||||
this.isProcessingDrop = false
|
||||
this.isDraggingFromHex = false
|
||||
}
|
||||
},
|
||||
async handleEmptyReserveDrop(event) {
|
||||
this.emptyReserveDragOver = false
|
||||
try {
|
||||
const data = JSON.parse(event.dataTransfer.getData('application/json'))
|
||||
if (data.source === 'hex') {
|
||||
await this.handleBadgeReturn(data)
|
||||
}
|
||||
} catch {
|
||||
// ignore invalid drag data
|
||||
}
|
||||
},
|
||||
async swapBadges(sourceBadge, sourceIndex, targetBadge, targetIndex) {
|
||||
// Mutation 1: Move source badge to target slot (target badge goes to unused)
|
||||
await this.setSlot(sourceBadge, targetIndex)
|
||||
try {
|
||||
// Mutation 2: Move former target badge to now-empty source slot
|
||||
await this.setSlot(targetBadge, sourceIndex)
|
||||
} catch {
|
||||
this.$toast.error(this.$t('settings.badges.swap-partial-error'))
|
||||
return
|
||||
}
|
||||
this.$toast.success(this.$t('settings.badges.success-update'))
|
||||
},
|
||||
async setSlot(badge, slot) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: setTrophyBadgeSelected,
|
||||
@ -143,15 +253,21 @@ export default {
|
||||
} catch (error) {
|
||||
this.$toast.error(this.$t('settings.badges.error-update'))
|
||||
}
|
||||
|
||||
this.$refs.badgesComponent.resetSelection()
|
||||
this.selectedBadgeIndex = null
|
||||
// Keep the slot selected so the (now empty) slot immediately shows
|
||||
// available badges, including the one that was just removed.
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.badge-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.presenterContainer {
|
||||
margin-top: 20px;
|
||||
padding-top: 50px;
|
||||
@ -179,4 +295,34 @@ export default {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.drag-instruction {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.reserve-title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.empty-reserve-drop-zone {
|
||||
margin-top: 20px;
|
||||
padding: 24px;
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.empty-reserve-drop-zone.reserve-drag-over {
|
||||
border-color: $color-success;
|
||||
background-color: rgba($color-success, 0.05);
|
||||
color: $color-success;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user