add media order controls
This commit is contained in:
parent
faed935436
commit
543ee6628d
2 changed files with 130 additions and 5 deletions
|
|
@ -43,6 +43,7 @@ export const useMediaSetsStore = defineStore('mediaSets', () => {
|
|||
const isCreating = ref(false)
|
||||
const isUpdating = ref(false)
|
||||
const isDeleting = ref(false)
|
||||
const isReordering = ref(false)
|
||||
const createError = ref<string | null>(null)
|
||||
const updateError = ref<string | null>(null)
|
||||
const deleteError = ref<string | null>(null)
|
||||
|
|
@ -187,6 +188,31 @@ export const useMediaSetsStore = defineStore('mediaSets', () => {
|
|||
}
|
||||
}
|
||||
|
||||
async function reorderSets(setIds: number[]): Promise<boolean> {
|
||||
isReordering.value = true
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/sets/order`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ setIds }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return false
|
||||
}
|
||||
|
||||
const data: SetsResponse = await response.json()
|
||||
sets.value = data.sets
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
} finally {
|
||||
isReordering.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function errorMessage(
|
||||
response: Response,
|
||||
fallbackMessage: string,
|
||||
|
|
@ -206,6 +232,7 @@ export const useMediaSetsStore = defineStore('mediaSets', () => {
|
|||
isCreating,
|
||||
isUpdating,
|
||||
isDeleting,
|
||||
isReordering,
|
||||
createError,
|
||||
updateError,
|
||||
deleteError,
|
||||
|
|
@ -213,5 +240,6 @@ export const useMediaSetsStore = defineStore('mediaSets', () => {
|
|||
createSet,
|
||||
updateSet,
|
||||
deleteSet,
|
||||
reorderSets,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { storeToRefs } from 'pinia'
|
|||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import {
|
||||
AlertTriangle as AlertTriangleIcon,
|
||||
ChevronDown as ChevronDownIcon,
|
||||
ChevronUp as ChevronUpIcon,
|
||||
Pencil as PencilIcon,
|
||||
Plus as PlusIcon,
|
||||
Trash2 as TrashIcon,
|
||||
|
|
@ -18,6 +20,8 @@ interface CreateSetForm {
|
|||
description: string
|
||||
}
|
||||
|
||||
type SetMoveDirection = 'up' | 'down'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const mediaSetsStore = useMediaSetsStore()
|
||||
|
|
@ -28,6 +32,7 @@ const {
|
|||
isCreating,
|
||||
isUpdating,
|
||||
isDeleting,
|
||||
isReordering,
|
||||
createError,
|
||||
updateError,
|
||||
deleteError,
|
||||
|
|
@ -42,6 +47,7 @@ const localCreateError = ref<string | null>(null)
|
|||
const localEditError = ref<string | null>(null)
|
||||
const editingSet = ref<MediaSet | null>(null)
|
||||
const deletingSet = ref<MediaSet | null>(null)
|
||||
const setOrderStatus = ref<string | null>(null)
|
||||
|
||||
const createSetForm = reactive<CreateSetForm>({
|
||||
name: '',
|
||||
|
|
@ -231,6 +237,42 @@ async function handleDeleteSet(): Promise<void> {
|
|||
deletingSet.value = null
|
||||
}
|
||||
|
||||
async function handleMoveSet(
|
||||
setId: number,
|
||||
direction: SetMoveDirection,
|
||||
): Promise<void> {
|
||||
const currentIndex = sets.value.findIndex((mediaSet) => {
|
||||
return mediaSet.id === setId
|
||||
})
|
||||
if (currentIndex === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const targetIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1
|
||||
if (targetIndex < 0 || targetIndex >= sets.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const reorderedSets = [...sets.value]
|
||||
const movingSet = reorderedSets[currentIndex]
|
||||
const targetSet = reorderedSets[targetIndex]
|
||||
if (movingSet === undefined || targetSet === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
reorderedSets[currentIndex] = targetSet
|
||||
reorderedSets[targetIndex] = movingSet
|
||||
const setIds = reorderedSets.map((mediaSet) => {
|
||||
return mediaSet.id
|
||||
})
|
||||
|
||||
setOrderStatus.value = null
|
||||
const saved = await mediaSetsStore.reorderSets(setIds)
|
||||
if (saved) {
|
||||
setOrderStatus.value = 'Set order saved'
|
||||
}
|
||||
}
|
||||
|
||||
function openSet(mediaSet: MediaSet): void {
|
||||
if (mediaSet.rootElementId === null) {
|
||||
return
|
||||
|
|
@ -287,6 +329,14 @@ function resetEditSetForm(): void {
|
|||
</header>
|
||||
|
||||
<section class="media-page__sets" aria-label="Featured sets">
|
||||
<p
|
||||
v-if="setOrderStatus !== null"
|
||||
class="media-page__status media-page__order-status"
|
||||
data-cy="media-set-order-status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{{ setOrderStatus }}
|
||||
</p>
|
||||
<p v-if="isLoading" class="media-page__status">Loading media sets...</p>
|
||||
<p
|
||||
v-else-if="error"
|
||||
|
|
@ -302,7 +352,7 @@ function resetEditSetForm(): void {
|
|||
No media sets are available yet.
|
||||
</p>
|
||||
<div v-else class="media-page__grid" data-cy="media-set-grid">
|
||||
<template v-for="mediaSet in sets" :key="mediaSet.id">
|
||||
<template v-for="(mediaSet, setIndex) in sets" :key="mediaSet.id">
|
||||
<article
|
||||
v-if="authStore.isAuthenticated"
|
||||
class="media-page__card media-page__card--managed"
|
||||
|
|
@ -350,6 +400,34 @@ function resetEditSetForm(): void {
|
|||
<TrashIcon :size="18" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="media-page__card-order-actions"
|
||||
aria-label="Set order actions"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="media-page__card-action"
|
||||
data-cy="media-set-move-up"
|
||||
:disabled="isReordering || setIndex === 0"
|
||||
:aria-label="`Move ${mediaSet.name} up`"
|
||||
:title="`Move ${mediaSet.name} up`"
|
||||
@click="handleMoveSet(mediaSet.id, 'up')"
|
||||
>
|
||||
<ChevronUpIcon :size="18" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="media-page__card-action"
|
||||
data-cy="media-set-move-down"
|
||||
:disabled="isReordering || setIndex === sets.length - 1"
|
||||
:aria-label="`Move ${mediaSet.name} down`"
|
||||
:title="`Move ${mediaSet.name} down`"
|
||||
@click="handleMoveSet(mediaSet.id, 'down')"
|
||||
>
|
||||
<ChevronDownIcon :size="18" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
<RouterLink
|
||||
v-else-if="mediaSet.rootElementId !== null"
|
||||
|
|
@ -697,6 +775,12 @@ function resetEditSetForm(): void {
|
|||
border-color: #e5b8b8;
|
||||
}
|
||||
|
||||
.media-page__order-status {
|
||||
margin-bottom: 1.25rem;
|
||||
color: var(--color-olive);
|
||||
border-color: #c8d0ba;
|
||||
}
|
||||
|
||||
.media-page__grid {
|
||||
display: grid;
|
||||
max-width: 1745px;
|
||||
|
|
@ -762,6 +846,14 @@ a.media-page__card:focus-visible {
|
|||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.media-page__card-order-actions {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.media-page__card-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -776,21 +868,26 @@ a.media-page__card:focus-visible {
|
|||
box-shadow: 0 8px 18px rgb(44 44 44 / 8%);
|
||||
}
|
||||
|
||||
.media-page__card-action:hover,
|
||||
.media-page__card-action:focus-visible {
|
||||
.media-page__card-action:not(:disabled):hover,
|
||||
.media-page__card-action:not(:disabled):focus-visible {
|
||||
color: var(--color-olive);
|
||||
border-color: #d4ad5f;
|
||||
outline: 3px solid rgb(212 173 95 / 24%);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.media-page__card-action--danger:hover,
|
||||
.media-page__card-action--danger:focus-visible {
|
||||
.media-page__card-action--danger:not(:disabled):hover,
|
||||
.media-page__card-action--danger:not(:disabled):focus-visible {
|
||||
color: #9f2d2d;
|
||||
border-color: #d9a3a3;
|
||||
outline-color: rgb(159 45 45 / 18%);
|
||||
}
|
||||
|
||||
.media-page__card-action:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.media-page__card-icon {
|
||||
display: block;
|
||||
width: 140px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue