Files
kite/frontend/src/components/documents/DocumentCard.vue
T
curo1305 cce70b2ef6 refactor(frontend): extract shared modules, thin views, delete dead code
Shared utilities:
- Add src/utils/formatters.js — formatDate, formatSize, providerColor,
  providerBg, providerLabel; all components import from here, no inline duplicates
- Add src/components/ui/TreeItem.vue — generic expand/collapse tree node;
  FolderTreeItem, CloudFolderTreeItem, CloudProviderTreeItem now wrap it
- Add src/components/storage/StorageBrowser.vue — unified file browser grid
  used by both FileManagerView and CloudFolderView

View refactor (thin data-providers):
- FileManagerView.vue: stripped to props + event wiring; all layout moved to StorageBrowser
- CloudFolderView.vue: same treatment — feeds props into StorageBrowser
- All tree sidebar components delegate expand/collapse to TreeItem.vue

Dead code removed:
- Delete HomeView.vue — no active route, replaced by FileManagerView
- Delete FolderView.vue — no active route, logic merged into FileManagerView

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:10:47 +02:00

142 lines
5.4 KiB
Vue

<template>
<div
class="group bg-white border border-gray-200 rounded-xl p-4 hover:border-indigo-300 hover:shadow-sm transition-all cursor-pointer relative"
@click="$router.push(`/document/${doc.id}`)"
>
<div class="flex items-start gap-3">
<!-- Icon -->
<div class="w-9 h-9 rounded-lg bg-indigo-50 flex items-center justify-center shrink-0 mt-0.5">
<svg class="w-5 h-5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="font-medium text-gray-900 text-sm truncate">{{ doc.original_name }}</p>
<p class="text-xs text-gray-400 mt-0.5">{{ formatDate(doc.created_at) }} · {{ formatSize(doc.size_bytes) }}</p>
<!-- Topics -->
<div class="flex flex-wrap gap-1 mt-2">
<TopicBadge
v-for="topicName in doc.topics"
:key="topicName"
:name="topicName"
:color="topicColor(topicName)"
/>
<span v-if="!doc.topics?.length" class="text-xs text-gray-300 italic">unclassified</span>
</div>
<!-- Shared indicator pill -->
<div v-if="doc.is_shared" class="mt-2">
<span class="bg-indigo-50 text-indigo-600 text-xs font-medium px-2 py-1 rounded-full">Shared</span>
</div>
</div>
<!-- Action buttons (hover-reveal) -->
<div class="opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-1 shrink-0">
<!-- Move to folder -->
<div class="relative">
<button
@click.stop="toggleFolderPicker"
aria-label="Move to folder"
class="min-h-[44px] min-w-[44px] flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
</svg>
</button>
<!-- Folder picker dropdown -->
<div
v-if="showFolderPicker"
class="absolute right-0 top-full mt-1 w-48 bg-white border border-gray-200 rounded-xl shadow-lg z-20 py-1"
@click.stop
>
<button
class="w-full text-left px-3 py-2 text-sm text-gray-600 hover:bg-gray-50"
@click.stop="moveToFolder(null)"
>Root (no folder)</button>
<button
v-for="folder in allFolders"
:key="folder.id"
class="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-indigo-50 hover:text-indigo-600 truncate"
@click.stop="moveToFolder(folder.id)"
>{{ folder.name }}</button>
<p v-if="!allFolders.length" class="px-3 py-2 text-xs text-gray-400">No folders yet</p>
</div>
</div>
<!-- Share -->
<button
@click.stop="openShareModal"
aria-label="Share document"
class="min-h-[44px] min-w-[44px] flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
</svg>
</button>
</div>
</div>
<!-- ShareModal -->
<ShareModal
v-if="showShareModal"
:doc="doc"
@close="showShareModal = false"
@unshared="doc.is_shared = false"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useTopicsStore } from '../../stores/topics.js'
import { useFoldersStore } from '../../stores/folders.js'
import { moveDocument } from '../../api/client.js'
import TopicBadge from '../topics/TopicBadge.vue'
import ShareModal from '../sharing/ShareModal.vue'
import { formatDate, formatSize } from '../../utils/formatters.js'
const props = defineProps({
doc: Object,
})
const topicsStore = useTopicsStore()
const foldersStore = useFoldersStore()
const showShareModal = ref(false)
const showFolderPicker = ref(false)
const allFolders = computed(() => foldersStore.rootFolders)
function openShareModal() {
showShareModal.value = true
}
function toggleFolderPicker() {
showFolderPicker.value = !showFolderPicker.value
}
function closeFolderPicker(e) {
showFolderPicker.value = false
}
onMounted(() => document.addEventListener('click', closeFolderPicker))
onUnmounted(() => document.removeEventListener('click', closeFolderPicker))
async function moveToFolder(folderId) {
showFolderPicker.value = false
try {
await moveDocument(props.doc.id, folderId)
} catch (e) {
console.error('Move failed:', e.message)
}
}
function topicColor(name) {
return topicsStore.topics.find(t => t.name === name)?.color ?? '#6366f1'
}
</script>