diff --git a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts
index 760ee87..eb40d27 100644
--- a/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts
+++ b/frontend/rabbi_gerzi/cypress/e2e/admin-element.cy.ts
@@ -12,26 +12,17 @@ function loginAsAdmin(): void {
function fillElementForm(
title: string,
description: string,
- iconImageUrl: string,
richText: string,
- pdfPath: string,
youtubeUrl: string,
): void {
cy.get('[data-cy="admin-element-title"]').clear().type(title)
cy.get('[data-cy="admin-element-description"]').clear().type(description)
- cy.get('[data-cy="admin-element-icon-image-url"]')
- .clear()
- .type(iconImageUrl)
cy.get('[data-cy="admin-element-rich-text"]').clear()
if (richText !== '') {
cy.get('[data-cy="admin-element-rich-text"]').type(richText, {
parseSpecialCharSequences: false,
})
}
- cy.get('[data-cy="admin-element-pdf-path"]').clear()
- if (pdfPath !== '') {
- cy.get('[data-cy="admin-element-pdf-path"]').type(pdfPath)
- }
cy.get('[data-cy="admin-element-youtube-url"]').clear()
if (youtubeUrl !== '') {
cy.get('[data-cy="admin-element-youtube-url"]').type(youtubeUrl)
@@ -79,7 +70,6 @@ describe('admin element editing', () => {
it('saves element edits and restores the seed content through the UI', () => {
const originalTitle = 'Baderech HaAvodah'
const originalDescription = 'a structured path for inner and outer growth'
- const originalIconImageUrl = '/assets/baderech-haavodah-icon.png'
const updatedTitle = 'Baderech HaAvodah Updated'
const updatedDescription = 'Updated admin description'
const updatedRichText = '
Updated admin rich text
'
@@ -90,10 +80,8 @@ describe('admin element editing', () => {
fillElementForm(
updatedTitle,
updatedDescription,
- originalIconImageUrl,
updatedRichText,
'',
- '',
)
cy.get('[data-cy="admin-element-save"]').click()
@@ -110,8 +98,6 @@ describe('admin element editing', () => {
fillElementForm(
originalTitle,
originalDescription,
- originalIconImageUrl,
- '',
'',
'',
)
diff --git a/frontend/rabbi_gerzi/src/stores/elements.ts b/frontend/rabbi_gerzi/src/stores/elements.ts
index 9da179f..bf01744 100644
--- a/frontend/rabbi_gerzi/src/stores/elements.ts
+++ b/frontend/rabbi_gerzi/src/stores/elements.ts
@@ -22,9 +22,7 @@ interface ElementResponse {
export interface UpdateElementInput {
title: string
description: string
- iconImageUrl: string
richText: string
- pdfPath: string
youtubeUrl: string
}
@@ -32,6 +30,15 @@ interface UpdateElementResponse {
element: Element
}
+interface ElementPatchInput {
+ title?: string
+ description?: string
+ iconImageUrl?: string
+ richText?: string
+ pdfPath?: string
+ youtubeUrl?: string
+}
+
interface ErrorResponse {
error?: string
}
@@ -44,6 +51,8 @@ export const useElementsStore = defineStore('elements', () => {
const isLoading = ref(false)
const error = ref(null)
const isSaving = ref(false)
+ const isUploadingIconImage = ref(false)
+ const isUploadingPdf = ref(false)
const saveError = ref(null)
async function fetchElement(elementId: string): Promise {
@@ -80,6 +89,22 @@ export const useElementsStore = defineStore('elements', () => {
}
async function updateElement(elementId: string, input: UpdateElementInput): Promise {
+ return await saveElementPatch(elementId, input, 'Could not save element')
+ }
+
+ async function clearElementIconImage(elementId: string): Promise {
+ return await saveElementPatch(elementId, { iconImageUrl: '' }, 'Could not remove icon image')
+ }
+
+ async function clearElementPdf(elementId: string): Promise {
+ return await saveElementPatch(elementId, { pdfPath: '' }, 'Could not remove PDF')
+ }
+
+ async function saveElementPatch(
+ elementId: string,
+ input: ElementPatchInput,
+ failureMessage: string,
+ ): Promise {
saveError.value = null
isSaving.value = true
@@ -93,46 +118,117 @@ export const useElementsStore = defineStore('elements', () => {
body: JSON.stringify(input),
})
- if (response.status === 401) {
- saveError.value = 'Please log in again'
- return false
- }
-
- if (response.status === 404) {
- saveError.value = 'Element not found'
- return false
- }
-
- if (response.status === 400) {
- const data: ErrorResponse = await response.json()
- saveError.value = data.error ?? 'Could not save element'
- return false
- }
-
- if (!response.ok) {
- saveError.value = 'Could not save element'
- return false
- }
-
- const data: UpdateElementResponse = await response.json()
- element.value = data.element
- return true
+ return await handleElementResponse(response, failureMessage)
} catch {
- saveError.value = 'Network error - could not save element'
+ saveError.value = `Network error - ${failureMessage.toLowerCase()}`
return false
} finally {
isSaving.value = false
}
}
+ async function uploadElementIconImage(elementId: string, file: File): Promise {
+ saveError.value = null
+ isUploadingIconImage.value = true
+
+ try {
+ const encodedElementId = encodeURIComponent(elementId)
+ const elementUrl = `${API_BASE_URL}/api/elements/${encodedElementId}`
+ const formData = new FormData()
+ formData.append('iconImage', file)
+ const response = await fetch(`${elementUrl}/icon-image`, {
+ method: 'POST',
+ headers: { Accept: 'application/json' },
+ credentials: 'include',
+ body: formData,
+ })
+
+ return await handleElementResponse(response, 'Could not upload icon image')
+ } catch {
+ saveError.value = 'Network error - could not upload icon image'
+ return false
+ } finally {
+ isUploadingIconImage.value = false
+ }
+ }
+
+ async function uploadElementPdf(elementId: string, file: File): Promise {
+ saveError.value = null
+ isUploadingPdf.value = true
+
+ try {
+ const encodedElementId = encodeURIComponent(elementId)
+ const elementUrl = `${API_BASE_URL}/api/elements/${encodedElementId}`
+ const formData = new FormData()
+ formData.append('pdf', file)
+ const response = await fetch(`${elementUrl}/pdf`, {
+ method: 'POST',
+ headers: { Accept: 'application/json' },
+ credentials: 'include',
+ body: formData,
+ })
+
+ return await handleElementResponse(response, 'Could not upload PDF')
+ } catch {
+ saveError.value = 'Network error - could not upload PDF'
+ return false
+ } finally {
+ isUploadingPdf.value = false
+ }
+ }
+
+ async function handleElementResponse(
+ response: Response,
+ failureMessage: string,
+ ): Promise {
+ if (response.status === 401) {
+ saveError.value = 'Please log in again'
+ return false
+ }
+
+ if (response.status === 404) {
+ saveError.value = 'Element not found'
+ return false
+ }
+
+ if (response.status === 400) {
+ saveError.value = await errorMessage(response, failureMessage)
+ return false
+ }
+
+ if (!response.ok) {
+ saveError.value = failureMessage
+ return false
+ }
+
+ const data: UpdateElementResponse = await response.json()
+ element.value = data.element
+ return true
+ }
+
+ async function errorMessage(response: Response, fallbackMessage: string): Promise {
+ try {
+ const data: ErrorResponse = await response.json()
+ return data.error ?? fallbackMessage
+ } catch {
+ return fallbackMessage
+ }
+ }
+
return {
element,
childElements,
isLoading,
error,
isSaving,
+ isUploadingIconImage,
+ isUploadingPdf,
saveError,
fetchElement,
updateElement,
+ uploadElementIconImage,
+ uploadElementPdf,
+ clearElementIconImage,
+ clearElementPdf,
}
})
diff --git a/frontend/rabbi_gerzi/src/views/AdminElementPage.vue b/frontend/rabbi_gerzi/src/views/AdminElementPage.vue
index cc19049..37f03ff 100644
--- a/frontend/rabbi_gerzi/src/views/AdminElementPage.vue
+++ b/frontend/rabbi_gerzi/src/views/AdminElementPage.vue
@@ -8,23 +8,24 @@ import { useElementsStore } from '@/stores/elements'
interface ElementForm {
title: string
description: string
- iconImageUrl: string
richText: string
- pdfPath: string
youtubeUrl: string
}
const route = useRoute()
const elementsStore = useElementsStore()
-const { element, isLoading, error, isSaving, saveError } = storeToRefs(elementsStore)
+const { element, isLoading, error, isSaving, isUploadingIconImage, isUploadingPdf, saveError } =
+ storeToRefs(elementsStore)
const savedMessage = ref(null)
+const iconImageStatus = ref(null)
+const pdfStatus = ref(null)
+const iconImageInput = ref(null)
+const pdfInput = ref(null)
const form = reactive({
title: '',
description: '',
- iconImageUrl: '',
richText: '',
- pdfPath: '',
youtubeUrl: '',
})
@@ -54,6 +55,8 @@ watch(
}
savedMessage.value = null
+ iconImageStatus.value = null
+ pdfStatus.value = null
void elementsStore.fetchElement(currentElementId)
},
{ immediate: true },
@@ -66,9 +69,7 @@ watch(element, (currentElement) => {
form.title = currentElement.title
form.description = currentElement.description
- form.iconImageUrl = currentElement.iconImageUrl ?? ''
form.richText = currentElement.richText
- form.pdfPath = currentElement.pdfPath ?? ''
form.youtubeUrl = currentElement.youtubeUrl ?? ''
})
@@ -81,9 +82,7 @@ async function handleSubmit(): Promise {
const saved = await elementsStore.updateElement(elementId.value, {
title: form.title,
description: form.description,
- iconImageUrl: form.iconImageUrl,
richText: form.richText,
- pdfPath: form.pdfPath,
youtubeUrl: form.youtubeUrl,
})
@@ -91,6 +90,92 @@ async function handleSubmit(): Promise {
savedMessage.value = 'Saved'
}
}
+
+function chooseIconImage(): void {
+ iconImageInput.value?.click()
+}
+
+function choosePdf(): void {
+ pdfInput.value?.click()
+}
+
+async function handleIconImageChange(changeEvent: Event): Promise {
+ const selectedFile = selectedInputFile(changeEvent)
+ if (selectedFile === null || elementId.value === '') {
+ return
+ }
+
+ savedMessage.value = null
+ iconImageStatus.value = null
+ const uploaded = await elementsStore.uploadElementIconImage(elementId.value, selectedFile)
+ resetInput(changeEvent)
+
+ if (uploaded) {
+ iconImageStatus.value = 'Icon image updated'
+ }
+}
+
+async function handlePdfChange(changeEvent: Event): Promise {
+ const selectedFile = selectedInputFile(changeEvent)
+ if (selectedFile === null || elementId.value === '') {
+ return
+ }
+
+ savedMessage.value = null
+ pdfStatus.value = null
+ const uploaded = await elementsStore.uploadElementPdf(elementId.value, selectedFile)
+ resetInput(changeEvent)
+
+ if (uploaded) {
+ pdfStatus.value = 'PDF updated'
+ }
+}
+
+async function handleRemoveIconImage(): Promise {
+ if (elementId.value === '') {
+ return
+ }
+
+ savedMessage.value = null
+ iconImageStatus.value = null
+ const removed = await elementsStore.clearElementIconImage(elementId.value)
+ if (removed) {
+ iconImageStatus.value = 'Icon image removed'
+ }
+}
+
+async function handleRemovePdf(): Promise {
+ if (elementId.value === '') {
+ return
+ }
+
+ savedMessage.value = null
+ pdfStatus.value = null
+ const removed = await elementsStore.clearElementPdf(elementId.value)
+ if (removed) {
+ pdfStatus.value = 'PDF removed'
+ }
+}
+
+function selectedInputFile(changeEvent: Event): File | null {
+ const target = changeEvent.target
+ if (!(target instanceof HTMLInputElement)) {
+ return null
+ }
+
+ if (target.files === null || target.files.length === 0) {
+ return null
+ }
+
+ return target.files.item(0)
+}
+
+function resetInput(changeEvent: Event): void {
+ const target = changeEvent.target
+ if (target instanceof HTMLInputElement) {
+ target.value = ''
+ }
+}
@@ -130,15 +215,53 @@ async function handleSubmit(): Promise {
/>
-
+ No icon image
+
+
+
+
+
+
+ {{ iconImageStatus }}
+
+
-
+
+ PDF
+
+ View current PDF
+
+ No PDF
+
+
+
+
+
+
+ {{ pdfStatus }}
+
+