import { ref, type Ref } from 'vue' import { defineStore } from 'pinia' export interface ChildElement { id: number title: string description: string } export interface Element extends ChildElement { iconImageUrl: string | null richText: string shortPdfPath: string | null longPdfPath: string | null youtubeUrl: string | null } type ElementFileType = 'iconImage' | 'shortPdf' | 'longPdf' interface ElementResponse { element: Element childElements: ChildElement[] siblingElements: ChildElement[] } export interface UpdateElementInput { title: string description: string richText: string youtubeUrl: string } export interface CreateChildElementInput { title: string } interface UpdateElementResponse { element: Element } interface ElementPatchInput { title?: string description?: string iconImageUrl?: string richText?: string shortPdfPath?: string longPdfPath?: string youtubeUrl?: string } interface ErrorResponse { error?: string } const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string const ELEMENTS_URL = `${API_BASE_URL}/api/elements` const ELEMENT_UPDATE_URL = `${API_BASE_URL}/api/element/update` export const useElementsStore = defineStore('elements', () => { const element = ref(null) const childElements = ref([]) const siblingElements = ref([]) const isLoading = ref(false) const error = ref(null) const isSaving = ref(false) const isUploadingIconImage = ref(false) const isUploadingShortPdf = ref(false) const isUploadingLongPdf = ref(false) const isManagingChildren = ref(false) const saveError = ref(null) const childActionError = ref(null) async function fetchElement(elementId: string): Promise { element.value = null childElements.value = [] siblingElements.value = [] error.value = null saveError.value = null childActionError.value = null isLoading.value = true try { const encodedElementId = encodeURIComponent(elementId) const elementUrl = `${API_BASE_URL}/api/elements/${encodedElementId}` const response = await fetch(elementUrl) if (response.status === 404) { error.value = 'Element not found' return } if (!response.ok) { error.value = 'Could not load element' return } const data: ElementResponse = await response.json() element.value = data.element childElements.value = data.childElements siblingElements.value = data.siblingElements } catch { childElements.value = [] siblingElements.value = [] error.value = 'Network error - could not load element' } finally { isLoading.value = false } } async function updateElement( elementId: string, input: UpdateElementInput, ): Promise { return await saveElementPatch(elementId, input, 'Could not save element') } async function createChildElement( parentElementId: string, input: CreateChildElementInput, ): Promise { childActionError.value = null isManagingChildren.value = true try { const encodedParentElementId = encodeURIComponent(parentElementId) const response = await fetch( `${ELEMENTS_URL}/${encodedParentElementId}/children`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(input), }, ) return await handleChildElementResponse( response, 'Could not add child element', ) } catch { childActionError.value = 'Network error - could not add child element' return null } finally { isManagingChildren.value = false } } async function deleteChildElement( parentElementId: string, childElementId: number, ): Promise { childActionError.value = null isManagingChildren.value = true try { const encodedParentElementId = encodeURIComponent(parentElementId) const encodedChildElementId = encodeURIComponent( childElementId.toString(), ) const response = await fetch( `${ELEMENTS_URL}/${encodedParentElementId}` + `/children/${encodedChildElementId}`, { method: 'DELETE', credentials: 'include', }, ) return await handleDeleteChildElementResponse(response, childElementId) } catch { childActionError.value = 'Network error - could not remove child element' return false } finally { isManagingChildren.value = false } } async function clearElementIconImage(elementId: string): Promise { return await saveElementPatch( elementId, { iconImageUrl: '' }, 'Could not remove icon image', ) } async function clearElementShortPdf(elementId: string): Promise { return await saveElementPatch( elementId, { shortPdfPath: '' }, 'Could not remove short PDF', ) } async function clearElementLongPdf(elementId: string): Promise { return await saveElementPatch( elementId, { longPdfPath: '' }, 'Could not remove long PDF', ) } async function saveElementPatch( elementId: string, input: ElementPatchInput, failureMessage: string, ): Promise { saveError.value = null isSaving.value = true try { const response = await fetch(ELEMENT_UPDATE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ elementId, ...input }), }) return await handleElementResponse(response, failureMessage) } catch { saveError.value = `Network error - ${failureMessage.toLowerCase()}` return false } finally { isSaving.value = false } } async function uploadElementIconImage( elementId: string, file: File, ): Promise { return await uploadElementFile( elementId, file, 'iconImage', isUploadingIconImage, 'Could not upload icon image', 'Network error - could not upload icon image', ) } async function uploadElementShortPdf( elementId: string, file: File, ): Promise { return await uploadElementFile( elementId, file, 'shortPdf', isUploadingShortPdf, 'Could not upload short PDF', 'Network error - could not upload short PDF', ) } async function uploadElementLongPdf( elementId: string, file: File, ): Promise { return await uploadElementFile( elementId, file, 'longPdf', isUploadingLongPdf, 'Could not upload long PDF', 'Network error - could not upload long PDF', ) } async function uploadElementFile( elementId: string, file: File, fileType: ElementFileType, isUploadingFile: Ref, failureMessage: string, networkErrorMessage: string, ): Promise { saveError.value = null isUploadingFile.value = true try { const formData = new FormData() formData.append('elementId', elementId) formData.append('fileType', fileType) formData.append('file', file) const response = await fetch(ELEMENT_UPDATE_URL, { method: 'POST', headers: { Accept: 'application/json' }, credentials: 'include', body: formData, }) return await handleElementResponse(response, failureMessage) } catch { saveError.value = networkErrorMessage return false } finally { isUploadingFile.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 handleChildElementResponse( response: Response, failureMessage: string, ): Promise { if (response.status === 401) { childActionError.value = 'Please log in again' return null } if (response.status === 400 || response.status === 404) { childActionError.value = await errorMessage(response, failureMessage) return null } if (!response.ok) { childActionError.value = failureMessage return null } const data: UpdateElementResponse = await response.json() return data.element } async function handleDeleteChildElementResponse( response: Response, childElementId: number, ): Promise { if (response.status === 401) { childActionError.value = 'Please log in again' return false } if (response.status === 400 || response.status === 404) { childActionError.value = await errorMessage( response, 'Could not remove child element', ) return false } if (!response.ok) { childActionError.value = 'Could not remove child element' return false } childElements.value = childElements.value.filter((childElement) => { return childElement.id !== childElementId }) 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, siblingElements, isLoading, error, isSaving, isUploadingIconImage, isUploadingShortPdf, isUploadingLongPdf, isManagingChildren, saveError, childActionError, fetchElement, updateElement, createChildElement, deleteChildElement, uploadElementIconImage, uploadElementShortPdf, uploadElementLongPdf, clearElementIconImage, clearElementShortPdf, clearElementLongPdf, } })