diff --git a/frontend/rabbi_gerzi/src/stores/mediaSets.ts b/frontend/rabbi_gerzi/src/stores/mediaSets.ts
index e869470..e669e85 100644
--- a/frontend/rabbi_gerzi/src/stores/mediaSets.ts
+++ b/frontend/rabbi_gerzi/src/stores/mediaSets.ts
@@ -15,11 +15,18 @@ export interface CreateMediaSetInput {
iconImage: File
}
+export interface UpdateMediaSetInput {
+ id: number
+ name: string
+ description: string
+ iconImage: File | null
+}
+
interface SetsResponse {
sets: MediaSet[]
}
-interface CreateSetResponse {
+interface SetResponse {
set: MediaSet
}
@@ -34,7 +41,11 @@ export const useMediaSetsStore = defineStore('mediaSets', () => {
const isLoading = ref(false)
const error = ref(null)
const isCreating = ref(false)
+ const isUpdating = ref(false)
+ const isDeleting = ref(false)
const createError = ref(null)
+ const updateError = ref(null)
+ const deleteError = ref(null)
async function fetchSets(): Promise {
error.value = null
@@ -83,7 +94,7 @@ export const useMediaSetsStore = defineStore('mediaSets', () => {
return null
}
- const data: CreateSetResponse = await response.json()
+ const data: SetResponse = await response.json()
sets.value = [...sets.value, data.set]
return data.set
@@ -95,13 +106,112 @@ export const useMediaSetsStore = defineStore('mediaSets', () => {
}
}
+ async function updateSet(
+ input: UpdateMediaSetInput,
+ ): Promise {
+ updateError.value = null
+ isUpdating.value = true
+
+ const formData = new FormData()
+ formData.append('name', input.name)
+ formData.append('description', input.description)
+ if (input.iconImage !== null) {
+ formData.append('iconImage', input.iconImage)
+ }
+
+ try {
+ const encodedSetId = encodeURIComponent(input.id.toString())
+ const response = await fetch(
+ `${API_BASE_URL}/api/sets/${encodedSetId}/update`,
+ {
+ method: 'POST',
+ credentials: 'include',
+ body: formData,
+ },
+ )
+
+ if (!response.ok) {
+ updateError.value = await errorMessage(
+ response,
+ 'Could not update media set',
+ )
+ return null
+ }
+
+ const data: SetResponse = await response.json()
+ sets.value = sets.value.map((mediaSet) => {
+ if (mediaSet.id === data.set.id) {
+ return data.set
+ }
+
+ return mediaSet
+ })
+
+ return data.set
+ } catch {
+ updateError.value = 'Network error - could not update media set'
+ return null
+ } finally {
+ isUpdating.value = false
+ }
+ }
+
+ async function deleteSet(setId: number): Promise {
+ deleteError.value = null
+ isDeleting.value = true
+
+ try {
+ const encodedSetId = encodeURIComponent(setId.toString())
+ const response = await fetch(`${API_BASE_URL}/api/sets/${encodedSetId}`, {
+ method: 'DELETE',
+ credentials: 'include',
+ })
+
+ if (!response.ok) {
+ deleteError.value = await errorMessage(
+ response,
+ 'Could not delete media set',
+ )
+ return false
+ }
+
+ sets.value = sets.value.filter((mediaSet) => {
+ return mediaSet.id !== setId
+ })
+ return true
+ } catch {
+ deleteError.value = 'Network error - could not delete media set'
+ return false
+ } finally {
+ isDeleting.value = false
+ }
+ }
+
+ async function errorMessage(
+ response: Response,
+ fallbackMessage: string,
+ ): Promise {
+ try {
+ const data: ErrorResponse = await response.json()
+ return data.error ?? fallbackMessage
+ } catch {
+ return fallbackMessage
+ }
+ }
+
return {
sets,
isLoading,
error,
isCreating,
+ isUpdating,
+ isDeleting,
createError,
+ updateError,
+ deleteError,
fetchSets,
createSet,
+ updateSet,
+ deleteSet,
}
})
diff --git a/frontend/rabbi_gerzi/src/views/MediaPage.vue b/frontend/rabbi_gerzi/src/views/MediaPage.vue
index 513a329..07f407b 100644
--- a/frontend/rabbi_gerzi/src/views/MediaPage.vue
+++ b/frontend/rabbi_gerzi/src/views/MediaPage.vue
@@ -1,11 +1,17 @@
@@ -143,8 +303,56 @@ function resetCreateSetForm(): void {
+
+
+ {{ mediaSet.name }}
+
+ {{ mediaSet.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Delete "{{ deletingSet.name }}"? This will permanently delete this
+ set and all elements and uploaded files inside it.
+
+
+
+
+ {{ deleteError }}
+
+
+
+
+
+
+
+
@@ -368,10 +732,65 @@ a.media-page__card:focus-visible {
outline-offset: 4px;
}
+.media-page__card--managed {
+ position: relative;
+ cursor: pointer;
+}
+
+.media-page__card--managed:hover:not(.media-page__card--disabled),
+.media-page__card--managed:focus-visible:not(.media-page__card--disabled) {
+ border-color: #d4ad5f;
+ box-shadow: 0 14px 35px rgb(44 44 44 / 10%);
+ transform: translateY(-2px);
+}
+
+.media-page__card--managed:focus-visible {
+ outline: 3px solid rgb(212 173 95 / 45%);
+ outline-offset: 4px;
+}
+
.media-page__card--disabled {
+ cursor: default;
opacity: 0.72;
}
+.media-page__card-actions {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ display: flex;
+ gap: 0.5rem;
+}
+
+.media-page__card-action {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.4rem;
+ height: 2.4rem;
+ padding: 0;
+ color: var(--color-slate);
+ background: var(--color-white);
+ border: 1px solid #e5cf9f;
+ border-radius: 9999px;
+ box-shadow: 0 8px 18px rgb(44 44 44 / 8%);
+}
+
+.media-page__card-action:hover,
+.media-page__card-action: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 {
+ color: #9f2d2d;
+ border-color: #d9a3a3;
+ outline-color: rgb(159 45 45 / 18%);
+}
+
.media-page__card-icon {
display: block;
width: 140px;
@@ -590,6 +1009,11 @@ a.media-page__card:focus-visible {
border: 1px solid var(--color-olive);
}
+.create-set-modal__submit--danger {
+ background: #9f2d2d;
+ border-color: #9f2d2d;
+}
+
.create-set-modal__secondary:disabled,
.create-set-modal__submit:disabled,
.create-set-modal__close:disabled {
@@ -597,6 +1021,33 @@ a.media-page__card:focus-visible {
opacity: 0.65;
}
+.delete-set-modal__body {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.85rem;
+ margin-top: 1.5rem;
+}
+
+.delete-set-modal__icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.75rem;
+ height: 2.75rem;
+ flex: 0 0 auto;
+ color: #9f2d2d;
+ background: #fef2f2;
+ border: 1px solid #fecaca;
+ border-radius: 9999px;
+}
+
+.delete-set-modal__warning {
+ margin: 0;
+ color: var(--color-text);
+ font-size: 0.98rem;
+ line-height: 1.55;
+}
+
@media (max-width: 768px) {
.media-page__main {
padding: 3rem 1rem;