port-nimara-client-portal/components/FilePreviewModal.vue

230 lines
5.5 KiB
Vue

<template>
<v-dialog
v-model="isOpen"
max-width="90vw"
max-height="90vh"
scrollable
>
<v-card v-if="file">
<v-card-title class="d-flex align-center justify-space-between">
<div class="d-flex align-center">
<v-icon class="mr-2">{{ file.icon }}</v-icon>
<span>{{ file.displayName }}</span>
</div>
<v-btn
icon
variant="text"
@click="closeModal"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pa-0">
<!-- Loading State -->
<div v-if="loading" class="text-center pa-10">
<v-progress-circular
indeterminate
color="primary"
size="64"
/>
<p class="mt-4">Loading preview...</p>
</div>
<!-- Error State -->
<div v-else-if="error" class="text-center pa-10">
<v-icon size="64" color="error">mdi-alert-circle</v-icon>
<p class="mt-4">{{ error }}</p>
</div>
<!-- Image Preview -->
<div v-else-if="isImage" class="image-preview-container">
<img
:src="previewUrl"
:alt="file.displayName"
class="image-preview"
@load="loading = false"
@error="handlePreviewError"
/>
</div>
<!-- PDF Preview -->
<div v-else-if="isPdf" class="pdf-preview-container">
<object
:data="previewUrl"
type="application/pdf"
width="100%"
height="100%"
@load="loading = false"
@error="handlePreviewError"
>
<p>PDF preview is not available. <a :href="previewUrl" target="_blank">Click here to view the PDF</a>.</p>
</object>
</div>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
color="primary"
variant="outlined"
@click="downloadFile"
prepend-icon="mdi-download"
>
Download
</v-btn>
<v-btn
color="primary"
variant="flat"
@click="closeModal"
>
Close
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
interface FileItem {
name: string;
size: number;
sizeFormatted: string;
lastModified: string;
extension: string;
icon: string;
displayName: string;
isFolder: boolean;
}
interface Props {
modelValue: boolean;
file: FileItem | null;
}
interface Emits {
(e: 'update:modelValue', value: boolean): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const loading = ref(false);
const error = ref('');
const previewUrl = ref('');
// Computed property for v-model binding
const isOpen = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value),
});
// Check if file is an image
const isImage = computed(() => {
if (!props.file) return false;
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'];
return imageExtensions.includes(props.file.extension.toLowerCase());
});
// Check if file is a PDF
const isPdf = computed(() => {
if (!props.file) return false;
return props.file.extension.toLowerCase() === 'pdf';
});
// Watch for file changes and load preview
watch(() => props.file, async (newFile) => {
if (newFile && props.modelValue) {
await loadPreview();
}
});
// Watch for dialog open and load preview
watch(() => props.modelValue, async (isOpen) => {
if (isOpen && props.file) {
await loadPreview();
}
});
// Load preview URL
const loadPreview = async () => {
if (!props.file) return;
loading.value = true;
error.value = '';
previewUrl.value = '';
try {
// For images and PDFs, use the proxy endpoint to avoid CORS issues
if (isImage.value || isPdf.value) {
// Use the proxy endpoint that serves the file directly
const proxyUrl = `/api/files/proxy-preview?fileName=${encodeURIComponent(props.file.name)}`;
console.log('Setting preview URL to:', proxyUrl);
previewUrl.value = proxyUrl;
// The loading state will be handled by the image/iframe onload event
} else {
throw new Error('File type does not support preview');
}
} catch (err: any) {
error.value = err.data?.statusMessage || err.message || 'Failed to load preview';
loading.value = false;
}
};
// Handle preview load error
const handlePreviewError = (event: any) => {
console.error('Preview load error:', event);
console.error('Current preview URL:', previewUrl.value);
error.value = 'Failed to load preview';
loading.value = false;
};
// Download file
const downloadFile = async () => {
if (!props.file) return;
try {
const response = await $fetch('/api/files/download', {
params: { fileName: props.file.name },
});
window.open(response.url, '_blank');
} catch (err) {
console.error('Failed to download file:', err);
}
};
// Close modal
const closeModal = () => {
isOpen.value = false;
previewUrl.value = '';
error.value = '';
};
</script>
<style scoped>
.image-preview-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
max-height: 70vh;
overflow: auto;
background-color: #f5f5f5;
}
.image-preview {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
.pdf-preview-container {
width: 100%;
height: 70vh;
overflow: hidden;
}
</style>