route element file uploads

Add short and long PDF paths to elements, keep pdfPath as a short PDF compatibility alias, and route generic file uploads by fileType.
This commit is contained in:
Yisroel Baum 2026-06-14 23:17:56 +03:00
parent 0d5783ba9c
commit 9bbabc7fa6
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
28 changed files with 686 additions and 214 deletions

View file

@ -14,13 +14,23 @@ interface ElementForm {
const route = useRoute()
const elementsStore = useElementsStore()
const { element, isLoading, error, isSaving, isUploadingIconImage, isUploadingPdf, saveError } =
storeToRefs(elementsStore)
const {
element,
isLoading,
error,
isSaving,
isUploadingIconImage,
isUploadingShortPdf,
isUploadingLongPdf,
saveError,
} = storeToRefs(elementsStore)
const savedMessage = ref<string | null>(null)
const iconImageStatus = ref<string | null>(null)
const pdfStatus = ref<string | null>(null)
const shortPdfStatus = ref<string | null>(null)
const longPdfStatus = ref<string | null>(null)
const iconImageInput = ref<HTMLInputElement | null>(null)
const pdfInput = ref<HTMLInputElement | null>(null)
const shortPdfInput = ref<HTMLInputElement | null>(null)
const longPdfInput = ref<HTMLInputElement | null>(null)
const form = reactive<ElementForm>({
title: '',
@ -56,7 +66,8 @@ watch(
savedMessage.value = null
iconImageStatus.value = null
pdfStatus.value = null
shortPdfStatus.value = null
longPdfStatus.value = null
void elementsStore.fetchElement(currentElementId)
},
{ immediate: true },
@ -95,8 +106,12 @@ function chooseIconImage(): void {
iconImageInput.value?.click()
}
function choosePdf(): void {
pdfInput.value?.click()
function chooseShortPdf(): void {
shortPdfInput.value?.click()
}
function chooseLongPdf(): void {
longPdfInput.value?.click()
}
async function handleIconImageChange(changeEvent: Event): Promise<void> {
@ -107,7 +122,10 @@ async function handleIconImageChange(changeEvent: Event): Promise<void> {
savedMessage.value = null
iconImageStatus.value = null
const uploaded = await elementsStore.uploadElementIconImage(elementId.value, selectedFile)
const uploaded = await elementsStore.uploadElementIconImage(
elementId.value,
selectedFile,
)
resetInput(changeEvent)
if (uploaded) {
@ -115,19 +133,41 @@ async function handleIconImageChange(changeEvent: Event): Promise<void> {
}
}
async function handlePdfChange(changeEvent: Event): Promise<void> {
async function handleShortPdfChange(changeEvent: Event): Promise<void> {
const selectedFile = selectedInputFile(changeEvent)
if (selectedFile === null || elementId.value === '') {
return
}
savedMessage.value = null
pdfStatus.value = null
const uploaded = await elementsStore.uploadElementPdf(elementId.value, selectedFile)
shortPdfStatus.value = null
const uploaded = await elementsStore.uploadElementShortPdf(
elementId.value,
selectedFile,
)
resetInput(changeEvent)
if (uploaded) {
pdfStatus.value = 'PDF updated'
shortPdfStatus.value = 'Short PDF updated'
}
}
async function handleLongPdfChange(changeEvent: Event): Promise<void> {
const selectedFile = selectedInputFile(changeEvent)
if (selectedFile === null || elementId.value === '') {
return
}
savedMessage.value = null
longPdfStatus.value = null
const uploaded = await elementsStore.uploadElementLongPdf(
elementId.value,
selectedFile,
)
resetInput(changeEvent)
if (uploaded) {
longPdfStatus.value = 'Long PDF updated'
}
}
@ -144,16 +184,29 @@ async function handleRemoveIconImage(): Promise<void> {
}
}
async function handleRemovePdf(): Promise<void> {
async function handleRemoveShortPdf(): Promise<void> {
if (elementId.value === '') {
return
}
savedMessage.value = null
pdfStatus.value = null
const removed = await elementsStore.clearElementPdf(elementId.value)
shortPdfStatus.value = null
const removed = await elementsStore.clearElementShortPdf(elementId.value)
if (removed) {
pdfStatus.value = 'PDF removed'
shortPdfStatus.value = 'Short PDF removed'
}
}
async function handleRemoveLongPdf(): Promise<void> {
if (elementId.value === '') {
return
}
savedMessage.value = null
longPdfStatus.value = null
const removed = await elementsStore.clearElementLongPdf(elementId.value)
if (removed) {
longPdfStatus.value = 'Long PDF removed'
}
}
@ -186,7 +239,9 @@ function resetInput(changeEvent: Event): void {
<h1 class="admin-element-page__heading">Edit Element</h1>
</header>
<p v-if="isLoading" class="admin-element-page__status">Loading element...</p>
<p v-if="isLoading" class="admin-element-page__status">
Loading element...
</p>
<p
v-else-if="error"
class="admin-element-page__status admin-element-page__status--error"
@ -194,7 +249,11 @@ function resetInput(changeEvent: Event): void {
>
{{ error }}
</p>
<form v-else-if="element" class="admin-element-page__form" @submit.prevent="handleSubmit">
<form
v-else-if="element"
class="admin-element-page__form"
@submit.prevent="handleSubmit"
>
<label class="admin-element-page__field">
<span class="admin-element-page__label">Title</span>
<input
@ -255,7 +314,10 @@ function resetInput(changeEvent: Event): void {
</div>
<p
v-if="iconImageStatus !== null"
class="admin-element-page__status admin-element-page__status--inline"
:class="[
'admin-element-page__status',
'admin-element-page__status--inline',
]"
data-cy="admin-element-icon-image-status"
aria-live="polite"
>
@ -274,53 +336,110 @@ function resetInput(changeEvent: Event): void {
</label>
<section class="admin-element-page__media-field">
<span class="admin-element-page__label">PDF</span>
<span class="admin-element-page__label">Short PDF</span>
<a
v-if="element.pdfPath !== null"
:href="element.pdfPath"
v-if="element.shortPdfPath !== null"
:href="element.shortPdfPath"
class="admin-element-page__pdf-link"
data-cy="admin-element-current-pdf"
data-cy="admin-element-current-short-pdf"
target="_blank"
rel="noreferrer"
>
View current PDF
View current short PDF
</a>
<p v-else class="admin-element-page__empty-media">No PDF</p>
<p v-else class="admin-element-page__empty-media">No short PDF</p>
<div class="admin-element-page__media-actions">
<input
ref="pdfInput"
ref="shortPdfInput"
class="admin-element-page__file-input"
data-cy="admin-element-pdf-input"
data-cy="admin-element-short-pdf-input"
type="file"
accept="application/pdf"
:disabled="isUploadingPdf"
@change="handlePdfChange"
:disabled="isUploadingShortPdf"
@change="handleShortPdfChange"
/>
<button
class="admin-element-page__secondary-button"
type="button"
:disabled="isUploadingPdf"
@click="choosePdf"
:disabled="isUploadingShortPdf"
@click="chooseShortPdf"
>
{{ isUploadingPdf ? 'Uploading...' : 'Choose PDF' }}
{{ isUploadingShortPdf ? 'Uploading...' : 'Choose short PDF' }}
</button>
<button
v-if="element.pdfPath !== null"
v-if="element.shortPdfPath !== null"
class="admin-element-page__secondary-button"
type="button"
:disabled="isSaving || isUploadingPdf"
@click="handleRemovePdf"
:disabled="isSaving || isUploadingShortPdf"
@click="handleRemoveShortPdf"
>
Remove PDF
Remove short PDF
</button>
</div>
<p
v-if="pdfStatus !== null"
class="admin-element-page__status admin-element-page__status--inline"
data-cy="admin-element-pdf-status"
v-if="shortPdfStatus !== null"
:class="[
'admin-element-page__status',
'admin-element-page__status--inline',
]"
data-cy="admin-element-short-pdf-status"
aria-live="polite"
>
{{ pdfStatus }}
{{ shortPdfStatus }}
</p>
</section>
<section class="admin-element-page__media-field">
<span class="admin-element-page__label">Long PDF</span>
<a
v-if="element.longPdfPath !== null"
:href="element.longPdfPath"
class="admin-element-page__pdf-link"
data-cy="admin-element-current-long-pdf"
target="_blank"
rel="noreferrer"
>
View current long PDF
</a>
<p v-else class="admin-element-page__empty-media">No long PDF</p>
<div class="admin-element-page__media-actions">
<input
ref="longPdfInput"
class="admin-element-page__file-input"
data-cy="admin-element-long-pdf-input"
type="file"
accept="application/pdf"
:disabled="isUploadingLongPdf"
@change="handleLongPdfChange"
/>
<button
class="admin-element-page__secondary-button"
type="button"
:disabled="isUploadingLongPdf"
@click="chooseLongPdf"
>
{{ isUploadingLongPdf ? 'Uploading...' : 'Choose long PDF' }}
</button>
<button
v-if="element.longPdfPath !== null"
class="admin-element-page__secondary-button"
type="button"
:disabled="isSaving || isUploadingLongPdf"
@click="handleRemoveLongPdf"
>
Remove long PDF
</button>
</div>
<p
v-if="longPdfStatus !== null"
:class="[
'admin-element-page__status',
'admin-element-page__status--inline',
]"
data-cy="admin-element-long-pdf-status"
aria-live="polite"
>
{{ longPdfStatus }}
</p>
</section>
@ -345,7 +464,10 @@ function resetInput(changeEvent: Event): void {
</button>
<p
v-if="statusMessage !== null"
:class="['admin-element-page__status', 'admin-element-page__status--inline']"
:class="[
'admin-element-page__status',
'admin-element-page__status--inline',
]"
data-cy="admin-element-status"
aria-live="polite"
>

View file

@ -242,15 +242,29 @@ function isShortYoutubeHost(hostname: string): boolean {
/>
</div>
<div v-if="element.pdfPath !== null" class="element-page__actions">
<div
v-if="element.shortPdfPath !== null || element.longPdfPath !== null"
class="element-page__actions"
>
<a
:href="element.pdfPath"
v-if="element.shortPdfPath !== null"
:href="element.shortPdfPath"
class="element-page__pdf-link"
data-cy="element-pdf-link"
data-cy="element-short-pdf-link"
target="_blank"
rel="noreferrer"
>
View PDF
View Short PDF
</a>
<a
v-if="element.longPdfPath !== null"
:href="element.longPdfPath"
class="element-page__pdf-link"
data-cy="element-long-pdf-link"
target="_blank"
rel="noreferrer"
>
View Long PDF
</a>
</div>
@ -360,7 +374,9 @@ function isShortYoutubeHost(hostname: string): boolean {
.element-page__actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
margin-top: 1.75rem;
}