feat: Redesign sidebar to a horizontal top navigation bar and enhance assets view with asset upload, refined filters, and a new grid layout.

This commit is contained in:
xds
2026-02-09 01:52:20 +03:00
parent c73bffc9f4
commit 27337e0ccf
4 changed files with 341 additions and 138 deletions

View File

@@ -25,8 +25,35 @@ const selectedAsset = ref<Asset | null>(null)
const isModalVisible = ref(false)
const first = ref(0)
const rows = ref(12)
const rows = ref(18)
const totalRecords = ref(0)
const fileInput = ref<HTMLInputElement | null>(null)
const triggerFileUpload = () => {
fileInput.value?.click()
}
const handleFileUpload = async (event: Event) => {
const target = event.target as HTMLInputElement
if (target.files && target.files[0]) {
const file = target.files[0]
try {
loading.value = true
await dataService.uploadAsset(file)
toast.add({ severity: 'success', summary: 'Success', detail: 'Asset uploaded successfully', life: 3000 })
// Reset to first page and reload
first.value = 0
activeFilter.value = 'uploaded' // Switch to uploaded view to see the new asset
loadAssets()
} catch (e) {
console.error('Failed to upload asset', e)
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to upload asset', life: 3000 })
} finally {
loading.value = false
target.value = '' // Reset input
}
}
}
const openModal = (asset: Asset) => {
selectedAsset.value = asset
@@ -114,92 +141,114 @@ const formatDate = (dateString: string) => {
<template>
<div class="flex flex-col h-full p-8 overflow-y-auto w-full text-slate-100">
<!-- Main Content (Sidebar removed) -->
<div class="flex-1 flex flex-col">
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Top Bar -->
<header class="flex justify-between items-end mb-8">
<header class="flex justify-between items-center mb-4 px-1">
<div>
<h1 class="text-4xl font-bold m-0">Assets Library</h1>
<p class="mt-2 mb-0 text-slate-400">Manage all your assets</p>
<h1
class="text-xl font-bold bg-gradient-to-r from-white to-slate-400 bg-clip-text text-transparent m-0">
Assets Library</h1>
<p class="text-xs text-slate-500 mt-1">Manage all your assets</p>
</div>
<div class="glass-panel p-2 flex gap-2 rounded-xl">
<Button v-for="filter in ['all', 'image']" :key="filter"
:label="filter.charAt(0).toUpperCase() + filter.slice(1)"
:class="activeFilter === filter ? 'bg-white/10 text-slate-50' : 'bg-transparent text-slate-400'"
class="px-4 py-2 rounded-lg font-medium transition-all duration-300 hover:text-slate-50" text
@click="handleFilterChange(filter)" />
<div class="flex items-center gap-2">
<input type="file" ref="fileInput" @change="handleFileUpload" class="hidden" accept="image/*" />
<Button label="Upload" icon="pi pi-upload" @click="triggerFileUpload"
class="!bg-violet-600 !border-none hover:!bg-violet-500 !text-xs !px-3 !py-1.5 !rounded-lg !font-medium shadow-lg shadow-violet-500/20" />
<div class="glass-panel p-1 flex gap-1 rounded-xl bg-slate-800/50 border border-white/5">
<Button v-for="filter in ['all', 'generated', 'uploaded']" :key="filter"
:label="filter.charAt(0).toUpperCase() + filter.slice(1)"
:class="activeFilter === filter ? 'bg-white/10 text-slate-50 shadow-sm' : 'bg-transparent text-slate-400 hover:text-slate-200'"
class="px-3 py-1.5 rounded-lg text-xs font-medium transition-all duration-200" text
@click="handleFilterChange(filter)" />
</div>
</div>
</header>
<!-- Loading State -->
<div v-if="loading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 pb-8">
<div v-for="i in 8" :key="i" class="glass-panel rounded-2xl overflow-hidden">
<Skeleton height="180px" />
<div class="p-5">
<Skeleton class="mb-2" />
<Skeleton width="60%" />
</div>
</div>
<div v-if="loading"
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-6 gap-2 md:gap-1 pb-4 overflow-y-auto custom-scrollbar">
<Skeleton v-for="i in 12" :key="i" class="aspect-[9/16] !bg-slate-800 rounded-none sm:rounded-md" />
</div>
<!-- Assets Grid -->
<div v-else class="flex-1 flex flex-col">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-6 pb-8">
<div v-for="asset in paginatedAssets" :key="asset.id" @click="openModal(asset)"
class="glass-panel rounded-2xl overflow-hidden transition-all duration-300 cursor-pointer border border-white/5 hover:-translate-y-1 hover:border-white/20 hover:shadow-2xl">
<!-- Media Preview -->
<div class="h-70 bg-black/30 relative overflow-hidden">
<img :src="(API_URL + asset.url + '?thumbnail=true') || 'https://via.placeholder.com/300'"
:alt="asset.name"
class="w-full h-full object-cover transition-transform duration-500 hover:scale-105" />
<div
class="absolute top-2.5 right-2.5 bg-black/60 backdrop-blur-sm px-3 py-1 rounded-full text-xs uppercase font-semibold text-white z-10">
{{ asset.type }}
</div>
<div @click.stop="confirmDelete(asset)"
class="absolute top-2.5 left-2.5 w-8 h-8 rounded-full bg-black/60 backdrop-blur-sm flex items-center justify-center cursor-pointer hover:bg-red-500/80 transition-all z-10 group/delete">
<i
class="pi pi-trash text-white text-xs group-hover/delete:scale-110 transition-transform"></i>
</div>
<div v-else class="flex-1 overflow-y-auto custom-scrollbar pb-20">
<div v-if="paginatedAssets.length > 0"
class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-6 gap-2 md:gap-1">
<div v-for="asset in paginatedAssets" :key="asset.id"
class="aspect-[9/16] relative group overflow-hidden bg-slate-800 transition-all duration-300 rounded-none sm:rounded-md border border-white/5 hover:border-white/20"
@click="openModal(asset)">
<!-- Image -->
<img :src="(API_URL + asset.url + '?thumbnail=true') || 'https://via.placeholder.com/300'"
:alt="asset.name"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
<!-- Type Badge (Optional, maybe too cluttered for 9:16 tiles, but kept for now) -->
<div v-if="asset.type !== 'image'"
class="absolute top-2 right-2 bg-black/60 backdrop-blur-sm px-1.5 py-0.5 rounded text-[10px] uppercase font-bold text-white z-10 opacity-70 group-hover:opacity-100 transition-opacity">
{{ asset.type }}
</div>
<!-- Asset Info -->
<div class="p-5">
<div class="flex justify-between items-start mb-2">
<h3
class="m-0 text-base font-semibold whitespace-nowrap overflow-hidden text-ellipsis flex-1 mr-2">
{{ asset.name }}
</h3>
</div>
<div class="flex justify-between items-center text-xs text-slate-400">
<span>{{ formatDate(asset.created_at) }}</span>
<!-- Hover Overlay -->
<div
class="absolute inset-0 bg-black/60 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex flex-col justify-between p-3">
<!-- Top Actions -->
<div
class="flex justify-between items-start translate-y-[-10px] group-hover:translate-y-0 transition-transform duration-200">
<Button icon="pi pi-trash" v-tooltip.right="'Delete'"
class="!w-7 !h-7 !rounded-full !bg-red-500/20 !border-none !text-red-400 text-xs hover:!bg-red-500 hover:!text-white transition-colors"
@click.stop="confirmDelete(asset)" />
<span v-if="asset.linked_char_id"
class="bg-emerald-500/10 text-emerald-400 px-2 py-0.5 rounded">
🔗 Linked
class="text-[10px] bg-emerald-500/20 text-emerald-400 px-2 py-1 rounded-full border border-emerald-500/20"
v-tooltip.left="'Linked to Character'">
Linked
</span>
</div>
<!-- Center View Button -->
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
<Button icon="pi pi-eye" rounded text
class="!bg-black/40 !text-white !w-10 !h-10 !rounded-full opacity-0 group-hover:opacity-100 hover:!bg-black/60 hover:!scale-110 transition-all duration-300 pointer-events-auto !border border-white/20"
@click.stop="openModal(asset)" />
</div>
<!-- Bottom Info -->
<div class="translate-y-[10px] group-hover:translate-y-0 transition-transform duration-200">
<p class="text-xs font-medium text-white line-clamp-1 mb-0.5">{{ asset.name }}</p>
<p class="text-[10px] text-slate-400">{{ formatDate(asset.created_at) }}</p>
</div>
</div>
</div>
</div>
<div v-else class="flex flex-col items-center justify-center h-64 text-slate-500">
<i class="pi pi-images text-4xl mb-3 opacity-50"></i>
<p>No assets found</p>
<Button v-if="activeFilter !== 'all'" label="Clear Filters" text class="mt-2 !text-violet-400"
@click="handleFilterChange('all')" />
</div>
<!-- Pagination -->
<div v-if="totalRecords > rows" class="mt-auto py-6">
<div v-if="totalRecords > rows" class="mt-6 flex justify-center">
<Paginator :first="first" :rows="rows" :totalRecords="totalRecords" @page="onPage" :template="{
default: 'FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink'
}" class="!bg-transparent !border-none !p-0" :pt="{
root: { class: '!bg-transparent' },
pcPageButton: {
root: ({ context }) => ({
class: [
'!min-w-[40px] !h-10 !rounded-xl !border-none !transition-all !duration-300 !font-bold',
context.active ? '!bg-violet-600 !text-white !shadow-lg' : '!bg-white/5 !text-slate-400 hover:!bg-white/10 hover:!text-slate-50'
]
})
},
pcFirstPageButton: { root: { class: '!bg-white/5 !text-slate-400 !border-none !rounded-xl !min-w-[40px] !h-10 hover:!bg-white/10 hover:!text-slate-50 transition-all' } },
pcPreviousPageButton: { root: { class: '!bg-white/5 !text-slate-400 !border-none !rounded-xl !min-w-[40px] !h-10 hover:!bg-white/10 hover:!text-slate-50 transition-all' } },
pcNextPageButton: { root: { class: '!bg-white/5 !text-slate-400 !border-none !rounded-xl !min-w-[40px] !h-10 hover:!bg-white/10 hover:!text-slate-50 transition-all' } },
pcLastPageButton: { root: { class: '!bg-white/5 !text-slate-400 !border-none !rounded-xl !min-w-[40px] !h-10 hover:!bg-white/10 hover:!text-slate-50 transition-all' } }
pages: { class: 'gap-1' },
pageButton: ({ context }) => ({
class: [
'!min-w-[32px] !h-8 !rounded-lg !border-none !transition-all !duration-200 !text-xs',
context.active ? '!bg-violet-600 !text-white !shadow-lg !shadow-violet-500/30' : '!bg-white/5 !text-slate-400 hover:!bg-white/10 hover:!text-slate-200'
]
}),
firstPageButton: { root: { class: '!bg-white/5 !text-slate-400 !border-none !rounded-lg !min-w-[32px] !h-8 hover:!bg-white/10 hover:!text-slate-200 transition-all !mr-1' } },
previousPageButton: { root: { class: '!bg-white/5 !text-slate-400 !border-none !rounded-lg !min-w-[32px] !h-8 hover:!bg-white/10 hover:!text-slate-200 transition-all !mr-2' } },
nextPageButton: { root: { class: '!bg-white/5 !text-slate-400 !border-none !rounded-lg !min-w-[32px] !h-8 hover:!bg-white/10 hover:!text-slate-200 transition-all !ml-2' } },
lastPageButton: { root: { class: '!bg-white/5 !text-slate-400 !border-none !rounded-lg !min-w-[32px] !h-8 hover:!bg-white/10 hover:!text-slate-200 transition-all !ml-1' } }
}" />
</div>
</div>