Files
panel/resources/js/components/ComponentsLibrary.vue
2025-08-04 16:33:07 +03:30

779 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="components-library-container">
<!-- Toggle Button -->
<VBtn
v-if="!isLibraryOpen"
@click="toggleLibrary"
class="library-toggle-btn"
color="primary"
variant="elevated"
size="large"
>
<VIcon icon="tabler-layout-dashboard" size="20" class="me-2" />
Components Library
<VIcon icon="tabler-chevron-up" size="16" class="ms-2" />
</VBtn>
<!-- Components Library Panel -->
<VCard
v-if="isLibraryOpen"
class="components-library-panel"
:class="{ 'panel-open': isLibraryOpen }"
elevation="8"
>
<VCardTitle class="library-header">
<div class="header-content">
<div class="header-left">
<VIcon icon="tabler-layout-dashboard" size="24" class="me-2" />
<span>Dashboard Components</span>
</div>
<VBtn
@click="toggleLibrary"
variant="text"
size="small"
icon
class="close-btn"
>
<VIcon icon="tabler-x" size="20" />
</VBtn>
</div>
</VCardTitle>
<VCardText class="library-content">
<div class="search-section">
<VTextField
v-model="searchQuery"
placeholder="Search components..."
variant="outlined"
density="compact"
prepend-inner-icon="tabler-search"
clearable
class="search-input"
/>
</div>
<div class="categories-tabs">
<VChipGroup
v-model="selectedCategory"
selected-class="text-primary"
class="category-chips"
>
<VChip
v-for="category in categories"
:key="category"
:value="category"
variant="outlined"
size="small"
>
{{ category }}
</VChip>
</VChipGroup>
</div>
<div class="components-grid">
<div
v-for="component in filteredComponents"
:key="component.id"
class="component-item"
:draggable="isEditMode"
@dragstart="handleDragStart($event, component)"
@dragend="handleDragEnd"
:class="{ 'draggable': isEditMode, 'disabled': !isEditMode }"
>
<div class="component-preview">
<VIcon :icon="component.icon" size="32" :color="component.color" />
<div v-if="isEditMode" class="component-overlay">
<VIcon icon="tabler-plus" size="20" color="white" />
</div>
<div v-if="!isEditMode" class="disabled-overlay">
<VIcon icon="tabler-lock" size="16" color="grey" />
</div>
</div>
<div class="component-info">
<h4 class="component-title">{{ component.title }}</h4>
<p class="component-description">{{ component.description }}</p>
<div class="component-meta">
<VChip
:color="component.color"
size="x-small"
variant="outlined"
class="component-type-chip"
>
{{ component.category }}
</VChip>
<VChip
size="x-small"
variant="outlined"
color="grey"
class="size-chip"
>
{{ component.defaultSize.cols }}×{{ Math.round(component.defaultSize.height) }}
</VChip>
</div>
</div>
</div>
</div>
<div v-if="filteredComponents.length === 0" class="no-results">
<VIcon icon="tabler-layout-dashboard" size="48" color="grey-lighten-1" />
<h3>No components found</h3>
<p>Try adjusting your search or category filter</p>
</div>
<div v-if="!isEditMode" class="edit-mode-notice">
<VAlert
type="info"
variant="tonal"
class="ma-2"
>
<template #prepend>
<VIcon icon="tabler-info-circle" />
</template>
<div>
<strong>Edit Mode Required</strong>
<br>
Enable edit mode to add components to your dashboard
</div>
</VAlert>
</div>
</VCardText>
</VCard>
<!-- Drop Zone Overlay -->
<div
v-if="isDragging"
class="drop-zone-overlay"
@dragover="handleDragOver"
@drop="handleDrop"
@dragleave="handleDragLeave"
>
<div class="drop-zone-content">
<VIcon icon="tabler-download" size="64" color="primary" />
<h2>Drop component here to add to dashboard</h2>
<p>Release to add the component to your dashboard</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps({
isEditMode: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['add-component'])
const isLibraryOpen = ref(false)
const isDragging = ref(false)
const searchQuery = ref('')
const selectedCategory = ref('All')
const categories = ['All', 'CRM', 'Ecommerce', 'Analytics', 'Reports', 'Activity']
const availableComponents = ref([
{
id: 'crm-active-project',
title: 'Active Project',
description: 'Shows current active project details',
icon: 'tabler-folder-open',
color: 'primary',
category: 'CRM',
component: 'CrmActiveProject',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-activity-timeline',
title: 'Activity Timeline',
description: 'Recent activities and updates timeline',
icon: 'tabler-timeline',
color: 'info',
category: 'Activity',
component: 'CrmActivityTimeline',
defaultSize: { cols: 6, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-analytics-sales',
title: 'Sales Analytics',
description: 'Sales performance and analytics',
icon: 'tabler-chart-line',
color: 'success',
category: 'Analytics',
component: 'CrmAnalyticsSales',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-earning-reports',
title: 'Earning Reports',
description: 'Yearly earning overview and reports',
icon: 'tabler-report-money',
color: 'warning',
category: 'Reports',
component: 'CrmEarningReportsYearlyOverview',
defaultSize: { cols: 8, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-project-status',
title: 'Project Status',
description: 'Current status of all projects',
icon: 'tabler-checkup-list',
color: 'deep-purple',
category: 'CRM',
component: 'CrmProjectStatus',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-recent-transactions',
title: 'Recent Transactions',
description: 'Latest financial transactions',
icon: 'tabler-credit-card',
color: 'teal',
category: 'CRM',
component: 'CrmRecentTransactions',
defaultSize: { cols: 6, height: 33.33 },
defaultProps: {}
},
{
id: 'crm-sales-countries',
title: 'Sales by Countries',
description: 'Geographic sales distribution',
icon: 'tabler-world',
color: 'indigo',
category: 'Analytics',
component: 'CrmSalesByCountries',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: {}
},
{
id: 'project-activity-bar',
title: 'Project Activity Chart',
description: 'Bar chart showing project activities',
icon: 'tabler-chart-bar',
color: 'cyan',
category: 'Analytics',
component: 'ProjectActivityBarChart',
defaultSize: { cols: 8, height: 33.33 },
defaultProps: {}
},
{
id: 'analysis-card-1',
title: 'Analysis Card (Projects)',
description: 'Active projects progress analysis',
icon: 'tabler-analytics',
color: 'pink',
category: 'Analytics',
component: 'AnalysisCard',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: { chartName: 'Active Projects Progress' }
},
{
id: 'analysis-card-2',
title: 'Analysis Card (Cost)',
description: 'Cost overview analysis',
icon: 'tabler-analytics',
color: 'pink',
category: 'Analytics',
component: 'AnalysisCard',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: { chartName: 'Cost overview' }
},
{
id: 'cost-overview',
title: 'Cost Overview',
description: 'Detailed cost breakdown and overview',
icon: 'tabler-coin',
color: 'orange',
category: 'Reports',
component: 'CostOverview',
defaultSize: { cols: 8, height: 33.33 },
defaultProps: {}
},
{
id: 'generated-leads-1',
title: 'Generated Leads (Primary)',
description: 'Lead generation progress - Primary theme',
icon: 'tabler-users',
color: 'primary',
category: 'Ecommerce',
component: 'GeneratedLeadsCard',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: { progress: 33 }
},
{
id: 'generated-leads-2',
title: 'Generated Leads (Success)',
description: 'Lead generation progress - Success theme',
icon: 'tabler-users',
color: 'success',
category: 'Ecommerce',
component: 'GeneratedLeadsCard',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: { donutColors: ["primary"], progress: 71 }
},
{
id: 'generated-leads-3',
title: 'Generated Leads (Warning)',
description: 'Lead generation progress - Warning theme',
icon: 'tabler-users',
color: 'warning',
category: 'Ecommerce',
component: 'GeneratedLeadsCard',
defaultSize: { cols: 4, height: 33.33 },
defaultProps: { donutColors: ["warning"], progress: 56 }
}
])
const filteredComponents = computed(() => {
let components = availableComponents.value
// Filter by category
if (selectedCategory.value !== 'All') {
components = components.filter(comp => comp.category === selectedCategory.value)
}
// Filter by search query
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
components = components.filter(comp =>
comp.title.toLowerCase().includes(query) ||
comp.description.toLowerCase().includes(query) ||
comp.category.toLowerCase().includes(query)
)
}
return components
})
const toggleLibrary = () => {
isLibraryOpen.value = !isLibraryOpen.value
}
const handleDragStart = (event, component) => {
if (!props.isEditMode) {
event.preventDefault()
return
}
isDragging.value = true
event.dataTransfer.setData('application/json', JSON.stringify(component))
event.dataTransfer.effectAllowed = 'copy'
// Create drag image
const dragImage = event.target.cloneNode(true)
dragImage.style.transform = 'rotate(5deg) scale(0.8)'
dragImage.style.opacity = '0.8'
dragImage.style.zIndex = '9999'
document.body.appendChild(dragImage)
event.dataTransfer.setDragImage(dragImage, 50, 50)
setTimeout(() => {
if (document.body.contains(dragImage)) {
document.body.removeChild(dragImage)
}
}, 0)
}
const handleDragEnd = () => {
isDragging.value = false
}
const handleDragOver = (event) => {
event.preventDefault()
event.dataTransfer.dropEffect = 'copy'
}
const handleDrop = (event) => {
event.preventDefault()
isDragging.value = false
try {
const componentData = JSON.parse(event.dataTransfer.getData('application/json'))
emit('add-component', componentData)
// Show success feedback
console.log('Component added successfully:', componentData.title)
} catch (error) {
console.error('Error adding component:', error)
}
}
const handleDragLeave = (event) => {
// Only hide if we're leaving the drop zone entirely
if (!event.currentTarget.contains(event.relatedTarget)) {
isDragging.value = false
}
}
// Close library when clicking outside
const handleClickOutside = (event) => {
if (isLibraryOpen.value && !event.target.closest('.components-library-panel, .library-toggle-btn')) {
isLibraryOpen.value = false
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
.components-library-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1000;
}
.library-toggle-btn {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1), 0 8px 32px rgba(var(--v-theme-primary), 0.2) !important;
border-radius: 12px !important;
padding: 12px 20px !important;
font-weight: 600 !important;
text-transform: none !important;
backdrop-filter: blur(10px);
animation: bounceIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.library-toggle-btn:hover {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15), 0 12px 40px rgba(var(--v-theme-primary), 0.3) !important;
}
.components-library-panel {
position: absolute;
bottom: 80px;
right: 0;
width: 450px;
max-height: 600px;
border-radius: 16px !important;
backdrop-filter: blur(20px);
background: rgba(255, 255, 255, 0.95) !important;
border: 1px solid rgba(255, 255, 255, 0.2);
animation: slideUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.library-header {
background: linear-gradient(135deg, rgba(var(--v-theme-primary), 0.1), rgba(var(--v-theme-secondary), 0.05));
border-radius: 16px 16px 0 0;
padding: 1rem 1.5rem;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.header-left {
display: flex;
align-items: center;
font-weight: 600;
font-size: 1.1rem;
}
.close-btn {
opacity: 0.7;
transition: all 0.2s ease;
}
.close-btn:hover {
opacity: 1;
transform: scale(1.1);
}
.library-content {
max-height: 500px;
overflow-y: auto;
padding: 1rem 1.5rem;
}
.search-section {
margin-bottom: 1rem;
}
.search-input {
border-radius: 12px;
}
.categories-tabs {
margin-bottom: 1.5rem;
}
.category-chips {
gap: 0.5rem;
}
.components-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
}
.component-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: rgba(255, 255, 255, 0.7);
transition: all 0.3s ease;
position: relative;
}
.component-item.draggable {
cursor: grab;
}
.component-item.draggable:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
border-color: rgba(var(--v-theme-primary), 0.3);
background: rgba(var(--v-theme-primary), 0.02);
}
.component-item.disabled {
opacity: 0.6;
cursor: not-allowed;
}
.component-item:active.draggable {
cursor: grabbing;
transform: scale(0.98);
}
.component-preview {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
background: rgba(var(--v-theme-surface), 0.8);
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
.component-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(var(--v-theme-primary), 0.9);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.2s ease;
}
.disabled-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(128, 128, 128, 0.3);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.component-item.draggable:hover .component-overlay {
opacity: 1;
}
.component-info {
flex: 1;
min-width: 0;
}
.component-title {
font-size: 0.95rem;
font-weight: 600;
margin: 0 0 0.25rem 0;
color: rgba(0, 0, 0, 0.87);
line-height: 1.2;
}
.component-description {
font-size: 0.8rem;
color: rgba(0, 0, 0, 0.6);
margin: 0 0 0.5rem 0;
line-height: 1.3;
}
.component-meta {
display: flex;
gap: 0.5rem;
align-items: center;
}
.component-type-chip,
.size-chip {
font-size: 0.7rem !important;
height: 20px !important;
}
.no-results {
text-align: center;
padding: 2rem 1rem;
color: rgba(0, 0, 0, 0.6);
}
.no-results h3 {
margin: 1rem 0 0.5rem 0;
font-size: 1.1rem;
}
.no-results p {
margin: 0;
font-size: 0.9rem;
}
.edit-mode-notice {
margin-top: 1rem;
}
.drop-zone-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(var(--v-theme-primary), 0.1);
backdrop-filter: blur(4px);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease;
}
.drop-zone-content {
text-align: center;
padding: 3rem;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
border: 3px dashed rgba(var(--v-theme-primary), 0.5);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
animation: dropZonePulse 1.5s ease-in-out infinite;
}
.drop-zone-content h2 {
margin: 1rem 0 0.5rem 0;
color: rgba(var(--v-theme-primary), 0.9);
font-size: 1.5rem;
font-weight: 600;
}
.drop-zone-content p {
margin: 0;
color: rgba(0, 0, 0, 0.7);
font-size: 1rem;
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3) translateY(100px);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes dropZonePulse {
0%, 100% {
transform: scale(1);
border-color: rgba(var(--v-theme-primary), 0.5);
}
50% {
transform: scale(1.02);
border-color: rgba(var(--v-theme-primary), 0.8);
}
}
@media (max-width: 768px) {
.components-library-container {
bottom: 1rem;
right: 1rem;
left: 1rem;
}
.components-library-panel {
width: 100%;
max-width: none;
right: 0;
left: 0;
}
.library-toggle-btn {
width: 100%;
justify-content: center;
}
.component-item {
padding: 0.75rem;
}
.component-preview {
width: 50px;
height: 50px;
}
.component-title {
font-size: 0.9rem;
}
.component-description {
font-size: 0.75rem;
}
}
</style>