Files
space-booking/frontend/src/components/AttachmentsList.vue
Claude Agent 0bf3e6a7e2 feat: complete UI redesign with dark mode, sidebar navigation, and modern design system
Implemented comprehensive UI overhaul with three-layer architecture:

Layer 1 - Theme System:
- CSS variables for light/dark themes (theme.css)
- Theme composable with light/dark/auto mode (useTheme.ts)
- Sidebar state management composable (useSidebar.ts)
- Refactored main.css to use CSS variables throughout

Layer 2 - Core Components:
- AppSidebar with collapsible navigation (desktop) and overlay (mobile)
- CollapsibleSection reusable component for expandable cards
- Restructured App.vue with new sidebar layout
- Integrated Lucide icons library (lucide-vue-next)

Layer 3 - Views & Components:
- Updated all 14 views with CSS variables and responsive design
- Replaced inline SVG with Lucide icon components
- Added collapsible sections to Dashboard, Admin pages, UserProfile
- Updated 3 shared components (BookingForm, SpaceCalendar, AttachmentsList)

Features:
- Dark/light/auto theme with persistent preference
- Collapsible sidebar (icons-only on desktop, overlay on mobile)
- Consistent color palette using CSS variables
- Full responsive design across all pages
- Modern minimalist aesthetic with Indigo accent color

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 21:27:05 +00:00

241 lines
5.3 KiB
Vue

<template>
<div class="attachments-list">
<h4 class="attachments-title">Attachments</h4>
<div v-if="loading" class="loading">Loading attachments...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else-if="attachments.length === 0" class="no-attachments">No attachments</div>
<ul v-else class="attachment-items">
<li v-for="attachment in attachments" :key="attachment.id" class="attachment-item">
<div class="attachment-info">
<span class="attachment-icon">📎</span>
<div class="attachment-details">
<a
:href="getDownloadUrl(attachment.id)"
class="attachment-name"
target="_blank"
:download="attachment.filename"
>
{{ attachment.filename }}
</a>
<span class="attachment-meta">
{{ formatFileSize(attachment.size) }} · Uploaded by {{ attachment.uploader_name }} ·
{{ formatDate(attachment.created_at) }}
</span>
</div>
</div>
<button
v-if="canDelete"
type="button"
class="btn-delete"
:disabled="deleting === attachment.id"
@click="handleDelete(attachment.id)"
>
{{ deleting === attachment.id ? 'Deleting...' : 'Delete' }}
</button>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { attachmentsApi, handleApiError } from '@/services/api'
import { useAuthStore } from '@/stores/auth'
import { formatDateTime as formatDateTimeUtil } from '@/utils/datetime'
import type { Attachment } from '@/types'
interface Props {
bookingId: number
canDelete?: boolean
}
interface Emits {
(e: 'deleted'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const authStore = useAuthStore()
const userTimezone = computed(() => authStore.user?.timezone || 'UTC')
const attachments = ref<Attachment[]>([])
const loading = ref(true)
const error = ref('')
const deleting = ref<number | null>(null)
const loadAttachments = async () => {
loading.value = true
error.value = ''
try {
attachments.value = await attachmentsApi.list(props.bookingId)
} catch (err) {
error.value = handleApiError(err)
} finally {
loading.value = false
}
}
const handleDelete = async (attachmentId: number) => {
if (!confirm('Are you sure you want to delete this attachment?')) {
return
}
deleting.value = attachmentId
try {
await attachmentsApi.delete(attachmentId)
attachments.value = attachments.value.filter(a => a.id !== attachmentId)
emit('deleted')
} catch (err) {
error.value = handleApiError(err)
} finally {
deleting.value = null
}
}
const getDownloadUrl = (attachmentId: number): string => {
return attachmentsApi.download(attachmentId)
}
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
}
const formatDate = (dateString: string): string => {
return formatDateTimeUtil(dateString, userTimezone.value)
}
onMounted(() => {
loadAttachments()
})
</script>
<style scoped>
.attachments-list {
margin-top: 24px;
}
.attachments-title {
font-size: 16px;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 12px 0;
}
.loading,
.error,
.no-attachments {
padding: 12px;
border-radius: var(--radius-sm);
font-size: 14px;
}
.loading,
.no-attachments {
background: var(--color-bg-tertiary);
color: var(--color-text-secondary);
}
.error {
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
color: var(--color-danger);
}
.attachment-items {
list-style: none;
padding: 0;
margin: 0;
}
.attachment-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
margin-bottom: 8px;
gap: 12px;
}
.attachment-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 0;
}
.attachment-icon {
font-size: 20px;
flex-shrink: 0;
}
.attachment-details {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.attachment-name {
font-size: 14px;
color: var(--color-accent);
text-decoration: none;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attachment-name:hover {
text-decoration: underline;
}
.attachment-meta {
font-size: 12px;
color: var(--color-text-secondary);
}
.btn-delete {
padding: 6px 12px;
background: var(--color-surface);
color: var(--color-danger);
border: 1px solid var(--color-danger);
border-radius: var(--radius-sm);
font-size: 13px;
cursor: pointer;
transition: all var(--transition-fast);
flex-shrink: 0;
}
.btn-delete:hover:not(:disabled) {
background: color-mix(in srgb, var(--color-danger) 5%, transparent);
}
.btn-delete:disabled {
opacity: 0.6;
cursor: not-allowed;
}
@media (max-width: 640px) {
.attachment-item {
flex-direction: column;
align-items: stretch;
}
.btn-delete {
width: 100%;
}
}
</style>